Android自动化测试技术Espresso的使用
配置修改设置
先启用开发者选项,再在开发者选项下,停用以下三项设置:窗口动画缩放过渡动画缩放Animator 时长缩放添加依赖
在app/build.gradle文件中添加依赖androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0" androidTestImplementation "androidx.test:runner:1.2.0" androidTestImplementation "androidx.test:rules:1.2.0" 复制代码
在app/build.gradle文件中的android.defaultConfig中添加testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 复制代码
注意:上面的依赖只能实现基本功能,如果你想使用所有的功能,则按下面的配置:
所有依赖 androidTestImplementation "androidx.test.ext:junit:1.1.1" androidTestImplementation "androidx.test.ext:truth:1.2.0" androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0" androidTestImplementation "androidx.test.espresso:espresso-contrib:3.2.0" androidTestImplementation "androidx.test:runner:1.2.0" androidTestImplementation "androidx.test:rules:1.2.0" androidTestImplementation "androidx.test.espresso:espresso-intents:3.2.0" implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.test.espresso:espresso-idling-resource:3.2.0" 复制代码
下面调用的方法如onView()等都是静态方法,可以通过import static XXX来直接调用,所有需要导入的静态方法如下:import static androidx.test.espresso.Espresso.*; import static androidx.test.espresso.action.ViewActions.*; import static androidx.test.espresso.assertion.ViewAssertions.*; import static androidx.test.espresso.intent.Intents.intended; import static androidx.test.espresso.intent.Intents.intending; import static androidx.test.espresso.intent.matcher.ComponentNameMatchers.*; import static androidx.test.espresso.intent.matcher.IntentMatchers.*; import static androidx.test.espresso.matcher.ViewMatchers.*; import static androidx.test.ext.truth.content.IntentSubject.assertThat; 复制代码Api组件
常用Api组件包括:Espresso - 用于与视图交互(通过 onView() 和 onData())的入口点。此外,还公开不一定与任何视图相关联的 API,如 pressBack()。ViewMatchers - 实现 Matcher<? super View> 接口的对象的集合。您可以将其中一个或多个对象传递给 onView() 方法,以在当前视图层次结构中找到某个视图。ViewActions - 可传递给 ViewInteraction.perform() 方法的 ViewAction 对象(如 click())的集合。ViewAssertions - 可传递给 ViewInteraction.check() 方法的 ViewAssertion 对象的集合。在大多数情况下,您将使用 matches 断言,它使用视图匹配器断言当前选定视图的状态。
大多数可用的 Matcher、ViewAction 和 ViewAssertion 实例如下图(来源官方文档):
常用的api实例pdf使用普通控件
示例:MainActivity 包含一个 Button 和一个 TextView。点击该按钮后,TextView 的内容会变为 "改变成功"。
使用 Espresso 进行测试方法如下:@RunWith(AndroidJUnit4.class) @LargeTest public class ChangeTextTest { @Rule public ActivityTestRule activityRule = new ActivityTestRule<>(MainActivity.class); @Test public void test_change_text(){ onView(withId(R.id.change)) .perform(click()); onView(withId(R.id.content)) .check(matches(withText("改变成功"))); } } 复制代码
onView()方法用来获取匹配的当前视图,注意匹配的视图只能有一个,否则会报错。
withId()方法用来搜索匹配的视图,类似的还有withText()、 withHint()等。
perform()方法用来执行某种操作,例如点击click() 、长按longClick() 、双击doubleClick()
check()用来将断言应用于当前选定的视图
matches()最常用的断言,它断言当前选定视图的状态。上面的示例就是断言id为content的View它是否和text为"改变成功"的View匹配AdapterView相关控件
与普通控件不同,AdapterView(常用的是ListView)只能将一部分子视图加载到当前视图层次结构中。简单的 onView() 搜索将找不到当前未加载的视图。Espresso 提供一个单独的 onData() 入口点,该入口点能够先加载相关适配器项目,并在对其或其任何子级执行操作之前使其处于聚焦状态。
示例:打开Spinner,选择一个特定的条目,然后验证 TextView 是否包含该条目。Spinner 会创建一个包含其内容的 ListView,因此需要onData()@RunWith(AndroidJUnit4.class) @LargeTest public class SpinnerTest { @Rule public ActivityTestRule activityRule = new ActivityTestRule<>(MainActivity.class); @Test public void test_spinner(){ String content = "学校"; //点击Spnner,显示项目 onView(withId(R.id.change)).perform(click()); //点击指定的内容 onData(allOf(is(instanceOf(String.class)), is(content))).perform(click()); //判断TextView是否包含指定内容 onView(withId(R.id.content)) .check(matches(withText(containsString(content)))); } } 复制代码
下图为AdapterView的继承关系图:自定义Matcher和ViewAction
在介绍RecyclerView的操作之前,我们先要看看如何自定义Matcher和ViewAction。自定义Matcher
Matcher是一个用来匹配视图的接口,常用的是它的两个实现类BoundedMatcher 和TypeSafeMatcher
BoundedMatcher:一些匹配的语法糖,可以让你创建一个给定的类型,而匹配的特定亚型的只有过程项匹配。 类型参数: - 匹配器的期望类型。- T的亚型
TypeSafeMatcher:内部实现了空检查,检查的类型,然后进行转换
示例:输入EditText值,如果值以000开头,则让内容为 "成功" 的TextView可见,否则让内容为 失败 的TextView可见. @RunWith(AndroidJUnit4.class) @LargeTest public class EditTextTest { @Rule public ActivityTestRule activityRule = new ActivityTestRule<>(MainActivity.class); @Test public void rightInput() { onView(withId(R.id.editText)) .check(matches(EditMatcher.isRight())) .perform(typeText("000123"), ViewActions.closeSoftKeyboard()); onView(withId(R.id.button)).perform(click()); onView(withId(R.id.textView_success)).check(matches(isDisplayed())); onView(withId(R.id.textView_fail)).check(matches(not(isDisplayed()))); } @Test public void errorInput() { onView(withId(R.id.editText)) .check(matches(EditMatcher.isRight())) .perform(typeText("003"), ViewActions.closeSoftKeyboard()); onView(withId(R.id.button)).perform(click()); onView(withId(R.id.textView_success)).check(matches(not(isDisplayed()))); onView(withId(R.id.textView_fail)).check(matches(isDisplayed())); } static class EditMatcher{ static Matcher isRight(){ //自定义Matcher return new BoundedMatcher(EditText.class) { @Override public void describeTo(Description description) { description.appendText("EditText不满足要求"); } @Override protected boolean matchesSafely(EditText item) { //在输入EditText之前,先判EditText是否可见以及hint是否为指定值 if (item.getVisibility() == View.VISIBLE && item.getText().toString().isEmpty()) return true; else return false; } }; } } } 复制代码自定义ViewAction
这个不太熟悉,这里就介绍一下实现ViewAction接口,要实现的方法的作用 /** *符合某种限制的视图 */ public Matcher getConstraints(); /** *返回视图操作的描述。 *说明不应该过长,应该很好地适应于一句话 */ public String getDescription(); /** * 执行给定的视图这个动作。 *PARAMS:uiController - 控制器使用与UI交互。 *view - 在采取行动的view。 不能为null */ public void perform(UiController uiController, View view); } 复制代码RecyclerView
RecyclerView 对象的工作方式与 AdapterView 对象不同,因此不能使用 onData() 方法与其交互。 要使用 Espresso 与 RecyclerView 交互,您可以使用 espresso-contrib 软件包,该软件包具有 RecyclerViewActions的集合,定义了用于滚动到相应位置或对项目执行操作的方法。
添加依赖androidTestImplementation "androidx.test.espresso:espresso-contrib:3.2.0" 复制代码
操作RecyclerView的方法有:scrollTo() - 滚动到匹配的视图。scrollToHolder() - 滚动到匹配的视图持有者。scrollToPosition() - 滚动到特定位置。actionOnHolderItem() - 对匹配的视图持有者执行视图操作。actionOnItem() - 对匹配的视图执行视图操作。actionOnItemAtPosition() - 在特定位置对视图执行视图操作。
示例:选中删除功能:点击 编辑 ,TextView内容转为 删除 ,同时RecycleView的条目出现选中框,勾选要删除的项,点击 删除 ,删除指定项,RecycleView的条目的选中框消失。@RunWith(AndroidJUnit4.class) @LargeTest public class RecyclerViewTest { @Rule public ActivityTestRule activityRule = new ActivityTestRule<>(RecyclerActivity.class); static class ClickCheckBoxAction implements ViewAction{ @Override public Matcher getConstraints() { return any(View.class); } @Override public String getDescription() { return null; } @Override public void perform(UiController uiController, View view) { CheckBox box = view.findViewById(R.id.checkbox); box.performClick();//点击 } } static class MatcherDataAction implements ViewAction{ private String require; public MatcherDataAction(String require) { this.require = require; } @Override public Matcher getConstraints() { return any(View.class); } @Override public String getDescription() { return null; } @Override public void perform(UiController uiController, View view) { TextView text = view.findViewById(R.id.text); assertThat("数据值不匹配",require,equalTo(text.getText().toString())); } } public void delete_require_data(){ //获取RecyclerView中显示的所有数据 List l = new ArrayList<>(activityRule.getActivity().getData()); //点击 编辑 ,判断text是否变成 删除 onView(withId(R.id.edit)) .perform(click()) .check(matches(withText("删除"))); //用来记录要删除的项, Random random = new Random(); int time = random.nextInt(COUNT); List data = new ArrayList<>(COUNT); for (int i = 0; i < COUNT; i++) { data.add(""); } for (int i = 0; i < time; i++) { //随机生成要删除的位置 int position = random.nextInt(COUNT); //由于再次点击会取消,这里用来记录最后确定要删除的项 if (data.get(position).equals("")) data.set(position,"测试数据"+position); else data.set(position,""); //调用RecyclerViewActions.actionOnItemAtPosition()方法,滑到指定位置 //在执行指定操作 onView(withId(R.id.recycler)). perform(RecyclerViewActions.actionOnItemAtPosition(position,new ClickCheckBoxAction())); } //点击 删除 ,判断text是否变成 编辑 onView(withId(R.id.edit)) .perform(click(),doubleClick()) .check(matches(withText("编辑"))); //删除无用的项 data.removeIf(s -> s.equals("")); //获取最后保存的项 l.removeAll(data); //依次判断保留的项是否还存在 for (int i = 0; i < l.size(); i++) { final String require = l.get(i); onView(withId(R.id.recycler)) .perform(RecyclerViewActions. actionOnItemAtPosition(i,new MatcherDataAction(require))); } } } 复制代码
注意:在MatcherDataAction中调用了assertThat(),这种方式是不建议的。这里是我没有找到更好的方式来实现这个测试。Intent
Espresso-Intents 是 Espresso 的扩展,支持对被测应用发出的 Intent 进行验证和打桩。
添加依赖:androidTestImplementation "androidx.test.ext:truth:1.2.0" androidTestImplementation "androidx.test.espresso:espresso-intents:3.2.0" 复制代码
在编写 Espresso-Intents 测试之前,要先设置 IntentsTestRule。这是 ActivityTestRule 类的扩展,可让您在功能界面测试中轻松使用 Espresso-Intents的API。IntentsTestRule 会在带有 @Test 注解的每个测试运行前初始化Espresso-Intents,并在每个测试运行后释放 Espresso-Intents。 @Rule public IntentsTestRule mActivityRule = new IntentsTestRule<>( MainActivity.class); 复制代码验证 Intent
示例:在EditText中,输入电话号码,点击拨打按键,拨打电话。@RunWith(AndroidJUnit4.class) @LargeTest public class IntentTest { //设置拨打电话的权限的环境 @Rule public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant("android.permission.CALL_PHONE"); @Rule public IntentsTestRule mActivityRule = new IntentsTestRule<>( MainActivity.class); @Test public void test_start_other_app_intent(){ String phoneNumber = "123456"; //输入电话号码 onView(withId(R.id.phone)) .perform(typeText(phoneNumber), ViewActions.closeSoftKeyboard()); //点击拨打 onView(withId(R.id.button)) .perform(click()); //验证Intent是否正确 intended(allOf( hasAction(Intent.ACTION_CALL), hasData(Uri.parse("tel:"+phoneNumber)))); } } 复制代码
intended():是Espresso-Intents 提供的用来验证Intent的方法
除此之外,还可以通过断言的方式来验证IntentIntent receivedIntent = Iterables.getOnlyElement(Intents.getIntents()); assertThat(receivedIntent) .extras() .string("phone") .isEqualTo(phoneNumber); 复制代码插桩
上述方式可以解决一般的Intent验证的操作,但是当我们需要调用startActivityForResult()方法去启动照相机获取照片时,如果使用一般的方式,我们就需要手动去点击拍照,这样就不算自动化测试了。
Espresso-Intents 提供了intending()方法来解决这个问题,它可以为使用 startActivityForResult() 启动的 Activity 提供桩响应。简单来说就是,它不会去启动照相机,而是返回你自己定义的Intent。@RunWith(AndroidJUnit4.class) @LargeTest public class TakePictureTest { public static BoundedMatcher hasDrawable() { return new BoundedMatcher(ImageView.class) { @Override public void describeTo(Description description) { description.appendText("has drawable"); } @Override public boolean matchesSafely(ImageView imageView) { return imageView.getDrawable() != null; } }; } @Rule public IntentsTestRule mIntentsRule = new IntentsTestRule<>( MainActivity.class); @Rule public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA); @Before public void stubCameraIntent() { Instrumentation.ActivityResult result = createImageCaptureActivityResultStub(); intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result); } @Test public void takePhoto_drawableIsApplied() { //先检查ImageView中是否已经设置了图片 onView(withId(R.id.image)).check(matches(not(hasDrawable()))); // 点击拍照 onView(withId(R.id.button)).perform(click()); // 判断ImageView中是否已经设置了图片 onView(withId(R.id.image)).check(matches(hasDrawable())); } private Instrumentation.ActivityResult createImageCaptureActivityResultStub() { //自己定义Intent Bundle bundle = new Bundle(); bundle.putParcelable("data", BitmapFactory.decodeResource( mIntentsRule.getActivity().getResources(), R.drawable.ic_launcher_round)); Intent resultData = new Intent(); resultData.putExtras(bundle); return new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData); } } 复制代码空闲资源
空闲资源表示结果会影响界面测试中后续操作的异步操作。通过向 Espresso 注册空闲资源,可以在测试应用时更可靠地验证这些异步操作。
添加依赖implementation "androidx.test.espresso:espresso-idling-resource:3.2.0" 复制代码
下面以Google的官方示例来介绍,如何使用:
第一步:创建SimpleIdlingResource类,用来实现IdlingResourcepublic class SimpleIdlingResource implements IdlingResource { @Nullable private volatile ResourceCallback mCallback; private AtomicBoolean mIsIdleNow = new AtomicBoolean(true); @Override public String getName() { return this.getClass().getName(); } /** *false 表示这里有正在进行的任务,而true表示异步任务完成 */ @Override public boolean isIdleNow() { return mIsIdleNow.get(); } @Override public void registerIdleTransitionCallback(ResourceCallback callback) { mCallback = callback; } public void setIdleState(boolean isIdleNow) { mIsIdleNow.set(isIdleNow); if (isIdleNow && mCallback != null) { //调用这个方法后,Espresso不会再检查isIdleNow()的状态,直接判断异步任务完成 mCallback.onTransitionToIdle(); } } } 复制代码
第二步:创建执行异步任务的类MessageDelayerclass MessageDelayer { private static final int DELAY_MILLIS = 3000; interface DelayerCallback { void onDone(String text); } static void processMessage(final String message, final DelayerCallback callback, @Nullable final SimpleIdlingResource idlingResource) { if (idlingResource != null) { idlingResource.setIdleState(false); } Handler handler = new Handler(); new Thread(()->{ try { Thread.sleep(DELAY_MILLIS); } catch (InterruptedException e) { e.printStackTrace(); } handler.post(new Runnable() { @Override public void run() { if (callback != null) { callback.onDone(message); if (idlingResource != null) { idlingResource.setIdleState(true); } } } }); }).start(); } } 复制代码
第三步:在MainActivity中通过点击按钮开启任务public class MainActivity extends AppCompatActivity implements View.OnClickListener, MessageDelayer.DelayerCallback { private TextView mTextView; private EditText mEditText; @Nullable private SimpleIdlingResource mIdlingResource; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.changeTextBt).setOnClickListener(this); mTextView = findViewById(R.id.textToBeChanged); mEditText = findViewById(R.id.editTextUserInput); } @Override public void onClick(View view) { final String text = mEditText.getText().toString(); if (view.getId() == R.id.changeTextBt) { mTextView.setText("正在等待"); MessageDelayer.processMessage(text, this, mIdlingResource); } } @Override public void onDone(String text) { mTextView.setText(text); } /** * 仅测试能调用,创建并返回新的SimpleIdlingResource */ @VisibleForTesting @NonNull public IdlingResource getIdlingResource() { if (mIdlingResource == null) { mIdlingResource = new SimpleIdlingResource(); } return mIdlingResource; } } 复制代码
第四步:创建测试用例@RunWith(AndroidJUnit4.class) @LargeTest public class ChangeTextBehaviorTest { private static final String STRING_TO_BE_TYPED = "Espresso"; private IdlingResource mIdlingResource; /** *注册IdlingResource实例 */ @Before public void registerIdlingResource() { ActivityScenario activityScenario = ActivityScenario.launch(MainActivity.class); activityScenario.onActivity((ActivityScenario.ActivityAction) activity -> { mIdlingResource = activity.getIdlingResource(); IdlingRegistry.getInstance().register(mIdlingResource); }); } @Test public void changeText_sameActivity() { onView(withId(R.id.editTextUserInput)) .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard()); onView(withId(R.id.changeTextBt)).perform(click()); //只需要注册IdlingResource实例,Espresso就会自动在这里等待,直到异步任务完成 //在执行下面的代码 onView(withId(R.id.textToBeChanged)).check(matches(withText(STRING_TO_BE_TYPED))); } //取消注册 @After public void unregisterIdlingResource() { if (mIdlingResource != null) { IdlingRegistry.getInstance().unregister(mIdlingResource); } } } 复制代码
不足:Espresso提供了一套先进的同步功能。不过,该框架的这一特性仅适用于在 MessageQueue 上发布消息的操作,如在屏幕上绘制内容的 View 子类。其他
Espresso还有在多进程、WebView、无障碍功能检查、多窗口等内容,这些我不太熟悉,建议自己看 安卓官方文档或者下面的官方示例。官方示例IntentsBasicSample:intended() 和 intending() 的基本用法。IdlingResourceSample:与后台作业同步。BasicSample:基本的 Espresso 示例。CustomMatcherSample:展示如何扩展 Espresso 以与 EditText 对象的 hint 属性匹配。DataAdapterSample:展示 Espresso 中适用于列表和 AdapterView 对象的 onData() 入口点。IntentsAdvancedSample:模拟用户使用相机获取位图。MultiWindowSample:展示如何将 Espresso 指向不同的窗口。RecyclerViewSample:Espresso 的 RecyclerView 操作。WebBasicSample:使用 Espresso-Web 与 WebView 对象交互。
作者:时代不变
链接:https://juejin.cn/post/6844904181111734279
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
诺贝尔化学奖花落三家,古迪纳夫97岁成诺奖史上最高年龄得主当下,手机成为我们生活的必须品,手机钱包钥匙更是被戏称为出门必备的三件套,现在我们的手机充电快,电量使用长久,与今年诺贝尔化学奖得主们的孜孜不倦分不开,他们推动了锂电池的发展变革,
美联储印钞机开动,每月购债600亿美元,这是要准备薅羊毛吗9月份中旬,美国货币市场突现流动性危机,造成美国国债市场隔夜回购利率飙升,利率比正常时期高出几倍,致使利率使用成本,触及联邦基准利率的区间上限。通俗一点讲就是美元荒,市场缺钱,造成
LOL的双败赛制是什么?双败赛制是一种新的淘汰赛制,和以往输掉一场比赛就被淘汰的普通赛制不同,参赛者只有输了两场比赛后才会没有争冠军的可能。双败赛制一般分为两个组胜者组和败者组。通常,第一轮比赛过后,获胜
学校要求打卡14天才能返校?学会这招再也不怕忘了现在仍然是疫情防控阶段,即使我们国家在这方面做得还算比较好,但是也不能疏忽大意,毕竟会有境外输入的风险。而学校也是为了响应国家下达的要求,中高风险地区暂缓返校,低风险地区连续打卡1
买华为还是买苹果?这几年,智能手机的发展速度可谓是飞一般,国产品牌也是纷纷出现。放在以前,我国的智能手机还是苹果三星等国外科技巨头的天下而现在,情况早已不同,华为小米oppovivo等纷纷崛起,国人
阅读量翻了两倍,收益还更低了?因为你没有注意这两个因素大家好,今天这篇文章发出来,是为了帮助大家在写文章的路上多学点技巧,尽量避免走弯路,以便你们能更快地成功。相信朋友们都接触过今日头条了,很多人在这个平台上通过选择自己适合的体裁进行
别再乱发文章了,停下来研究一下往往收获更大大家好,今天来分享一下我的经验之谈,也是结合自身经历的感悟,希望能对你们,特别是新手有所帮助。相信大家在头条上发文章时都遇到过一个这样的问题有些文章收益高,有些则很低。他们百思不得
如何一键打开行程卡?特殊时期,人人都应该学会前言疫情时期,相信大家居住的城市里,各大场所都要求扫码测体温才能进去,虽然显得有点麻烦,但也是为了所有人的安全着想。但是,有的时候商场门前很多人要进去,就会造成拥挤,可能当时你拿着
辅助驾驶的黄金期与长征路当消费者与玩家一起涌入,这市场还是蓝海吗?文Toretto越来越多的消费者愿意为智能汽车技术买单。根据最新发布的2021麦肯锡汽车消费者洞察显示,九成受访者认为辅助驾驶有意义,10
拜拜!创因科技帮电商品牌低价乱价作为成年人,有种魔咒叫小时候,隔壁家的小孩永远考得比你好长大了,你的朋友同事永远赚的比你多我叫王大力,每天都要经历早起送孩子上学,下班接孩子回家,上班心悬着,今年不景气,老担心班上
创因科技为品牌非授权销售提供解决方案自家的产品未经过授权就被销售,并且将产品低价销售出去,你的品牌还好吗?未经授权擅自销售,对于品牌方而言,公司无法核实其货物是否符合相关质量标准以及真伪,消费者购买非授权产品,无法提
白话NBA白话版篮网雄鹿前瞻来了,为什么篮网最终会胜出?文白话频道大白上回有球必应也让我聊过一段雄鹿和篮网的前瞻,不过上回那个很短,就一分半钟,有的朋友说没听过瘾,那今天在知乎来个长的。我先说我的结论,我个人更看好篮网会胜出。下面我跟你
白话NBA快船抢七战胜独行侠,东契奇注定成超巨文大白大家好,我是大白。欢迎来到我的白话频道。今天说一下独行侠和快船精彩的抢七大战。话不多说,咱先把脸给打了。因为这组系列赛,我是挺牛派,一直认为东契奇可以带领独行侠战胜快船。虽然
朱一龙,王志文,王阳,童瑶,张子贤,点评叛逆者五大演员大家好,我是大白,欢迎来到我的白话天下。今天聊一下最近一段时间热播的电视剧叛逆者。这两天大结局,这个剧也放完了。今天就来简单做个节目聊聊它。至于剧情呢,老实讲,我觉得也没什么特别好
跨境ERP无货源上货系统的功能大全实时整合订单,产品利润店铺财务状况,统计分析仓储费促销费赔偿等所有财务明细收录支持按ASINMSKU等维度全方位分析支持按日周月汇总分析,为运营策略提供可靠数据支撑进销存管理实时整
虾拍档货代ERP系统功能大全,支持私有部署贴牌虾拍档erp大量上新测试流量爆款,精准采集(价格关键词链接)多单店铺刊登,关键词设置,多种语言互译功能!虾拍档erp店铺商品管理,批量设置库存商品规格,查询商品采集链接,买手账号导
亚马逊ERP货代打单系统功能大全亚马逊ERP货代软件是致力服务亚马逊卖家的系统,功能齐全,服务有保障!亚马逊erp大量上新测试流量爆款,采集(价格关键词链接)多单店铺刊登,关键词设置,多种语言互译功能!ERP功能
痘痘怎样才算治好了?图中的变化就是治疗的标准1不长粉刺2没有炎症3痘坑变小或者没有了4毛孔变细以下是治疗方法治疗前后的变化对比图(截至VISA)1意料之中结果(1)痘痘治好了2意料之外的结果(1)毛孔
打印Excel常用几个设置小技巧你还不会?现在收藏还不晚上一期我们讲了Excel在打印时的几个设置小技巧,但由于篇幅限制,这一期我们接着来分享剩下的几个Excel表格设置小技巧,一起来学习吧!1。如果需要打印多页文件时,如果没有序号或是
还分不清楚打印照片的尺寸?这样选择才合适点击上方关注绘威打印,我专业,您轻松!现在大部分人都养成了随手拍下照片记录美好生活的习惯,将手机里的美照打印出来装上相框作为装饰,更有纪念价值。那么如果我们想用家里的打印机打印照片
国产打印机历经十一年,新的打印机市场定位在哪里?点击上方关注绘威打印,我专业,您轻松!2010年以前,打印机市场一直是被日美等国家垄断,足足垄断了三十余年。直到2010年,我国在人民大会堂发布了第一台我国完全自主知识产权的奔图打
白话NBA威少不是湖人的最佳答案,但也绝非毫无用处文大白大家好,我是大白。欢迎来到我的白话NBA。前一阵就传威少要去湖人,今天早上一醒来,就发现我的微信爆了,好多网友都让我聊聊威少,我一看预览对话框就知道这事成了。第一时间确认这个