SuperAPI AI Directory

miniprogram-automation

Testing & quality

View repository

Updated 10-15 23:44

About this skill

WeChat mini-program automation

Overview

Use miniprogram-automator to drive WeChat DevTools to complete page navigation, element queries, interactions, Mock, runtime monitoring, screenshots, and regression verification.

This skill defaults topractical reference typeoutput:

  • Prioritize providinga directly runnable standalone Node.js script template
  • Only when the user explicitly requests integration with a testing framework, then output the Jest version
  • First confirm the approach and prerequisites, then implement into script files

Default output structure

When triggered, prioritize answering according to this structure:

  1. First explain what you plan to do: which type of script will be generated, which inputs it depends on
  2. List the prerequisite check items: project directory, CLI path, security settings, page paths, selectors
  3. Provide the complete script: prioritize standalone scripts, supplement with the Jest version if necessary
  4. Provide the run commands: install dependencies, execution commands, optional parameters
  5. Provide verification points and precautions: waiting strategy, Mock restoration, closing connections, screenshot limitations

If the user has already provided the full path, pages, selectors, and target behavior, do not repeatedly interrogate; directly produce the script.

Step 1: First collect key inputs

If the user has not provided complete information, first fill in these fields:

1.1 The project directory that can be opened by DevTools

automator.launch({ projectPath }) in projectPath should bethe directory actually opened by DevTools。

Do not mechanically interpret it as the "source repository root directory":

  • Native mini-program: usually the directory containing project.config.json / app.json directory
  • Taro / uni-app / custom build chains: usually the one actually opened by DevToolsbuild output directory, for example dist/、build/、miniprogram/

When unsure, first ask the user:

  • "Do you usually open the source root directory in WeChat DevTools, or the compiled dist/build directory?"

1.2 WeChat DevTools CLI path

Common default paths:

  • macOS:/Applications/wechatwebdevtools.app/Contents/MacOS/cli
  • Windows:C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat

If DevTools is installed in the standard location,cliPath can be omitted, and the SDK will automatically search according to the default path.

If the user's environment is non-standard, first have the user confirm the actual path.

1.3 miniprogram-automator version

Must use 0.12.0 or above, otherwise CLI command format compatibility issues will be triggered.

Older versions (0.5.x etc.) use the deprecated cli --auto <path> syntax, which is no longer supported by current WeChat DevTools (3.x), manifesting as:

[error] { code: 31, message: "Error: Missing param 'project / appid'" }

Upgrade method:

npm install miniprogram-automator@latest --save-dev

New versions (0.12.x) use the correct cli auto --project <path> --auto-port <port> syntax.

1.4 Security settings

Before using automator.launch() , you must remind the user to check:

  • WeChat DevTools → Settings → Security Settings → enable service port

Without this step, the common manifestation of the script is:

  • launch failure
  • Connection timeout
  • CLI is executable but WebSocket cannot be established

1.5 Target page and assertion targets

At minimum confirm:

  • Page path, such as /pages/home/index
  • whether to reLaunch / navigateTo / switchTab
  • What to verify: text, class names, data, screenshots, logs, exceptions, Mock request results
  • Whether key selectors are stable

1.6 Output form

By default, first provide astandalone script. Prioritize Jest only in these scenarios:

  • The user explicitly mentions Jest / unit tests / regression suite / CI
  • The task requires beforeAll/afterAll, multi-case organization, batch assertions

Step 2: Choose the correct workflow

2.1 launch or connect

Three workflows, choose by scenario:

Scenario Recommended method
Fully automatic: the script starts the developer tools itself automator.launch()
Developer tools are already open, connect directly CLI v2 + automator.connect()
Developer tools are already open and the automation port is ready Directly use automator.connect()

Method one:automator.launch()(fully automatic launch)

The SDK internally calls the CLI to start the developer tools, no manual operation required. Two hard prerequisites:

  1. The developer tools must be completely exited (Cmd+Q), otherwise it reports Failed to launch wechat web devTools—launch cannot coexist with an already running instance.
  2. On first run, a "Trust Project" confirmation dialog pops up, which requires the user to click manually, so timeout it is recommended to set 120000(2 minutes).
miniProgram = await automator.launch({
  cliPath: CLI_PATH,
  projectPath: PROJECT_PATH,
  timeout: 120000,
})

Method two: CLI v2 + automator.connect()(developer tools already running)

When the developer tools already have the project open (and you do not want to close and reopen), use the CLI v2 command to enable the automation WebSocket port, then use connect() to connect.

automator.launch() Internally it still uses the old CLI format (cli --auto <path>), and may fail on some newer versions of the developer tools. If you encounter launch() an error, switching to this method is usually more stable.

const { spawnSync } = require('node:child_process')

const CLI_PATH = process.env.WECHAT_DEVTOOLS_CLI ||
  '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
const PROJECT_PATH = process.env.MINIPROGRAM_PROJECT_PATH || '/path/to/project'
const AUTO_PORT = Number(process.env.WECHAT_AUTO_PORT || 9420)

// Step 1:用 CLI v2 开启自动化 ws 端口
// 若开发者工具 HTTP 服务端口不是默认值,需通过 --port 传入
function enableAutomation(httpPort) {
  const args = ['auto', '--project', PROJECT_PATH, '--auto-port', String(AUTO_PORT)]
  if (httpPort) args.push('--port', String(httpPort))
  const result = spawnSync(CLI_PATH, args, { encoding: 'utf8', timeout: 20000 })
  return { success: result.status === 0, output: (result.stdout || '') + (result.stderr || '') }
}

const { success, output } = enableAutomation()
if (!success) throw new Error(`CLI auto 命令失败:\n${output}`)

// Step 2:connect 到自动化 ws 端点
const miniProgram = await automator.connect({
  wsEndpoint: `ws://127.0.0.1:${AUTO_PORT}`,
})

Port conflict issue: If the developer tools HTTP service port is not the default value (for example, due to multiple instances), the CLI v2 command adding --port will cause a conflict error. Method to detect the actual port: first call the CLI once with a placeholder port, and parse the actual port from the output. For a complete example, see Template E.


Method three: directly use automator.connect()(port already ready)

The developer tools have already manually enabled the automation port (via the tool menu or a previous CLI v2 call), connect directly:

const miniProgram = await automator.connect({
  wsEndpoint: 'ws://localhost:9420',
})

Note:connect() The miniProgram of the close() obtained from

2.2 Standalone script or Jest

Scenario Recommendation
Temporary verification, single-page debugging, building script tools Standalone Node.js script
Regression testing, multiple cases, team collaboration, CI Jest

2.3 How to choose a waiting strategy

The priority is as follows:

  1. await page.waitFor('稳定选择器')
  2. await page.waitFor(async () => 条件成立)
  3. await page.waitFor(数字毫秒) as a fallback

Do not rely entirely on fixed sleep(2000). Fixed waits can only serve as a fallback, not as the primary synchronization method.

Step 3: Core Rules

3.1 For page waits, prioritize waitFor

page.waitFor() supports three forms:

await page.waitFor('.page-title')
await page.waitFor(async () => (await page.$$('.loaded-item')).length > 0)
await page.waitFor(500)

Recommended approach: first wait using a selector or a real conditional assertion, then add a very short fallback wait. Do not write a condition function that always returns true .

3.2 Page-level selectors cannot penetrate custom components

This is the place where mistakes are most likely.

Wrong approach:

const input = await page.$('form-panel input')

Correct approach:

const panel = await page.$('form-panel')
const input = await panel.$('input')

Reason:page.$ / page.$$ 's query scope ispage root node, and it cannot directly select elements inside custom components; even using form-panel input such a descendant selector will not work. To select elements inside a component, first get the component host element, then use element.$ / element.$$ to continue querying within the component scope.

3.3 When reading input values, prioritize value() or property('value')

If the target is a native input / textarea:

const value = await input.value()
// 或
const value2 = await input.property('value')

If the target is acustom component instance, then consider:

const data = await panel.data()

Do not confuse "reading a native input box value" with "reading a component's internal data".data() is more suitable for component instances, not a general-purpose input box reading method.

3.4 When mocking WeChat APIs, prioritize the official mockWxMethod

If the task is to mock wx.request、wx.getLocation etc., prioritize using:

await miniProgram.mockWxMethod('request', (options = {}) => {
  const res = {
    data: { code: 0, list: [{ id: 1, title: 'Mock Item A' }] },
    statusCode: 200,
    header: { 'content-type': 'application/json' },
    cookies: [],
    errMsg: 'request:ok',
  }

  Promise.resolve().then(() => {
    if (typeof options.success === 'function') options.success(res)
    if (typeof options.complete === 'function') options.complete(res)
  })

  return {
    abort() {},
    onHeadersReceived() {},
    offHeadersReceived() {},
    onChunkReceived() {},
    offChunkReceived() {},
  }
})

Restore at the end:

await miniProgram.restoreWxMethod('request')

Additional notes:

  • mockWxMethod also supports directly passing a fixed result object; simple scenarios do not always require writing a function
  • If a function is passed in, the function body will be serialized and executed, so do not rely on closures referencing external variables
  • If you need to call the original wx method, you can use this.origin

Only when the user explicitly needs deeper runtime injection, or mockWxMethod cannot cover the target scenario, then consider evaluate()。

3.5 Screenshots use miniProgram.screenshot(), and are limited to the developer tool simulator

await miniProgram.screenshot({ path: '/abs/path/to/file.png' })

Additional notes:

  • Passing { path } will save the screenshot to a file
  • If no parameter is passed, it returns the base64 encoding of the image data, suitable for in-memory comparison or upload

Must remind:

  • Screenshots are only applicable todeveloper tool simulator
  • not applicable to real-device debugging screens
  • Before taking a screenshot, ensure the page has rendered stably

3.6 Cleanup actions must be placed in finally

Whether it is a standalone script or Jest, ensure:

  • Restore mocked wx methods
  • Unbind event listeners (if used)
  • Close the mini-program instance
try {
  // 执行测试逻辑
} finally {
  await miniProgram.restoreWxMethod('request').catch(() => {})
  await miniProgram.close().catch(() => {})
}

Template A: General skeleton for standalone scripts

When the user says "give me a script", "don't change the test framework yet", or "want to run it quickly", start from this template first.

const automator = require('miniprogram-automator')
const path = require('node:path')
const fs = require('node:fs/promises')

const CLI_PATH = process.env.WECHAT_DEVTOOLS_CLI || '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
const PROJECT_PATH = process.env.MINIPROGRAM_PROJECT_PATH || '/absolute/path/to/devtools-project'
const TARGET_PAGE = '/pages/home/index'  // 必须以 / 开头
const OUTPUT_DIR = path.resolve(process.cwd(), 'outputs')

async function main() {
  let miniProgram
  try {
    await fs.mkdir(OUTPUT_DIR, { recursive: true })

    // 前提:开发者工具已完全退出(Cmd+Q);首次运行需在界面确认信任项目
    miniProgram = await automator.launch({
      cliPath: CLI_PATH,
      projectPath: PROJECT_PATH,
      timeout: 120000,  // 给首次启动 + 用户确认信任留足时间
    })

    const page = await miniProgram.reLaunch(TARGET_PAGE)

    // 主同步手段优先用稳定选择器,固定等待只做短兜底
    await page.waitFor('.page-title')
    await page.waitFor(100)
    const title = await page.$('.page-title')
    const titleText = await title.text()

    console.log('当前标题:', titleText)

    await miniProgram.screenshot({
      path: path.join(OUTPUT_DIR, 'current-page.png'),
    })

    console.log('截图完成')
  } finally {
    if (miniProgram) {
      await miniProgram.close()
    }
  }
}

main().catch((error) => {
  console.error(error)
  process.exit(1)
})

Template B: Input and validation inside custom components

When the user mentions "there is an input in the component", "why can't page.$ select it", or "need to read value or data", use this template.

const automator = require('miniprogram-automator')

async function run() {
  let miniProgram
  try {
    miniProgram = await automator.launch({
      cliPath: process.env.WECHAT_DEVTOOLS_CLI || '/Applications/wechatwebdevtools.app/Contents/MacOS/cli',
      projectPath: process.env.MINIPROGRAM_PROJECT_PATH || '/absolute/path/to/devtools-project',
    })

    const page = await miniProgram.reLaunch('/pages/form/index')
    await page.waitFor(300)

    const panel = await page.$('form-panel')
    if (!panel) throw new Error('未找到 form-panel')

    const input = await panel.$('input')
    if (!input) throw new Error('未找到组件内 input')

    await input.tap()
    await input.input('13800138000')

    await page.waitFor(100)

    const value = await input.value()
    if (value !== '13800138000') {
      throw new Error(`输入框值不符合预期: ${value}`)
    }

    // panel 必须是自定义组件实例;原生组件或普通元素不适合用 data() 读取内部状态
    const panelData = await panel.data().catch(() => null)
    console.log('panel.data() =', panelData)
  } finally {
    if (miniProgram) await miniProgram.close()
  }
}

run().catch((error) => {
  console.error(error)
  process.exit(1)
})

When answering, clearly explain:

  • page.$ cannot cross component boundaries
  • Reading native input values takes priority value() / property('value')
  • To read a component's internal state, use the component instance's data()

Template C: Mock wx.request + listen console/exception

When the user wants to do regression scripts, API faking, offline verification, or exception monitoring, give this template first.

First verify the target page's actual feedback path: some pages only update after a request succeeds loading, print console, or pop up a toast, and do not render a list or table. The official demo's packageAPI/pages/network/request/request is this kind of page, so assertions should be designed around button state, logs, toast, or page data; do not assume out of thin air that the page will definitely render a list.

const automator = require('miniprogram-automator')

async function run() {
  let miniProgram
  const consoleEvents = []
  const exceptionEvents = []

  try {
    miniProgram = await automator.launch({
      cliPath: process.env.WECHAT_DEVTOOLS_CLI || '/Applications/wechatwebdevtools.app/Contents/MacOS/cli',
      projectPath: process.env.MINIPROGRAM_PROJECT_PATH || '/absolute/path/to/devtools-project',
    })

    miniProgram.on('console', (payload) => {
      consoleEvents.push(payload)
    })

    miniProgram.on('exception', (payload) => {
      exceptionEvents.push(payload)
    })

    await miniProgram.mockWxMethod('request', (options = {}) => {
      const res = {
        data: {
          code: 0,
          list: [
            { id: 1, title: 'Mock Item A' },
            { id: 2, title: 'Mock Item B' },
          ],
        },
        statusCode: 200,
        header: { 'content-type': 'application/json' },
        cookies: [],
        errMsg: 'request:ok',
      }

      Promise.resolve().then(() => {
        if (typeof options.success === 'function') options.success(res)
        if (typeof options.complete === 'function') options.complete(res)
      })

      return {
        abort() {},
        onHeadersReceived() {},
        offHeadersReceived() {},
        onChunkReceived() {},
        offChunkReceived() {},
      }
    })

    const page = await miniProgram.reLaunch('/packageAPI/pages/network/request/request')
    await page.waitFor('button')

    const button = await page.$('button')
    if (!button) {
      throw new Error('未找到 request 页面按钮')
    }

    await button.tap()
    await page.waitFor(100)

    const pageData = await page.data().catch(() => ({}))
    if (pageData.loading !== false) {
      throw new Error(`请求结束后 loading 状态异常: ${JSON.stringify(pageData)}`)
    }

    const successLogs = consoleEvents.filter((event) => {
      if (!event || typeof event !== 'object' || !Array.isArray(event.args)) return false
      return event.args.some((arg) => String(arg).includes('request success'))
    })

    if (!successLogs.length) {
      throw new Error(`未观察到 request success 日志: ${JSON.stringify(consoleEvents)}`)
    }

    const consoleErrors = consoleEvents.filter((event) => {
      return event && typeof event === 'object' && event.type === 'error'
    })

    if (consoleErrors.length) {
      throw new Error(`存在 console.error: ${JSON.stringify(consoleErrors)}`)
    }

    if (exceptionEvents.length) {
      throw new Error(`存在 exception: ${JSON.stringify(exceptionEvents)}`)
    }
  } finally {
    if (miniProgram) {
      await miniProgram.restoreWxMethod('request').catch(() => {})
      await miniProgram.close().catch(() => {})
    }
  }
}

run().catch((error) => {
  console.error(error)
  process.exit(1)
})

Additional notes:

  • console The payload received by the event callback usually contains type and args
  • When determining failure, filter by payload.type === 'error' first; do not count normal log as failures together

Template E: CLI v2 + connect() complete script

When the user says "the devtools are already open", "I don't want to restart the tool", or "launch errors out and I want another way", use this template.

const automator = require('miniprogram-automator')
const path = require('node:path')
const fs = require('node:fs/promises')
const { spawnSync } = require('node:child_process')

const CLI_PATH =
  process.env.WECHAT_DEVTOOLS_CLI ||
  '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'

// projectPath 是开发者工具实际打开的目录(含 project.config.json)
const PROJECT_PATH =
  process.env.MINIPROGRAM_PROJECT_PATH || '/absolute/path/to/project'

// 自动化 WebSocket 端口,默认 9420
const AUTO_PORT = Number(process.env.WECHAT_AUTO_PORT || 9420)

const TARGET_PAGE = '/pages/home/index'
const OUTPUT_DIR = path.resolve(process.cwd(), 'outputs')

/**
 * 探测开发者工具当前 HTTP 服务端口。
 * 原理:先用占位端口调用一次 CLI,从错误输出里解析实际端口。
 * 如果工具还没启动,返回 null(CLI 会自行启动)。
 */
function detectHttpPort() {
  if (process.env.WECHAT_DEVTOOLS_PORT) {
    return Number(process.env.WECHAT_DEVTOOLS_PORT)
  }
  try {
    const result = spawnSync(
      CLI_PATH,
      ['auto', '--project', PROJECT_PATH, '--port', '9999'],
      { encoding: 'utf8', timeout: 8000 },
    )
    const output = (result.stdout || '') + (result.stderr || '')
    const match = output.match(/started on http:\/\/127\.0\.0\.1:(\d+)/)
    if (match) return Number(match[1])
  } catch (_) {}
  return null
}

/**
 * 用 CLI v2 命令开启自动化 WebSocket 端口。
 * 命令:cli auto --project <path> --auto-port <port> [--port <httpPort>]
 */
function enableAutomation(httpPort) {
  const args = ['auto', '--project', PROJECT_PATH, '--auto-port', String(AUTO_PORT)]
  if (httpPort) args.push('--port', String(httpPort))
  const result = spawnSync(CLI_PATH, args, { encoding: 'utf8', timeout: 20000 })
  return {
    success: result.status === 0,
    output: (result.stdout || '') + (result.stderr || ''),
  }
}

async function main() {
  let miniProgram
  try {
    await fs.mkdir(OUTPUT_DIR, { recursive: true })

    // Step 1:探测 HTTP 端口,避免 CLI 因端口冲突报错
    const httpPort = detectHttpPort()
    console.log(httpPort ? `HTTP 端口: ${httpPort}` : '未检测到 HTTP 端口,由 CLI 启动')

    // Step 2:用 CLI v2 开启自动化 ws 端口
    const { success, output } = enableAutomation(httpPort)
    if (!success) throw new Error(`CLI auto 命令失败:\n${output}`)
    console.log('自动化端口已就绪')

    // Step 3:connect 到 ws 端点
    miniProgram = await automator.connect({
      wsEndpoint: `ws://127.0.0.1:${AUTO_PORT}`,
    })
    console.log('已连接')

    // Step 4:跳转页面并操作
    const page = await miniProgram.reLaunch(TARGET_PAGE)
    await page.waitFor('.page-title')
    await page.waitFor(200)

    await miniProgram.screenshot({ path: path.join(OUTPUT_DIR, 'page.png') })
    console.log('截图完成')
  } finally {
    if (miniProgram) {
      // connect 模式下 close() 只断开 ws 连接,不关闭开发者工具
      await miniProgram.close().catch(() => {})
    }
  }
}

main().catch((error) => {
  console.error(error)
  process.exit(1)
})

Additional notes:

  • connect() 's close() only disconnects the WebSocket connection, and does not close the devtools window—this differs from the launch() after close() behavior
  • --auto-port is the automation ws port (used for automator connections),--port is the tool HTTP service port (used for CLI management); the two are different
  • If you are not sure about the tool HTTP port, you can first use detectHttpPort() to probe; if the tool is not running, the CLI will start it itself

Template D: Batch screenshot script snippet

When the user wants to batch-screenshot multiple pages, do visual regression, or output a PNG list, this section can be reused.

const PAGES = [
  { id: 'home', path: '/pages/home/index' },
  { id: 'list', path: '/pages/list/index' },
]

for (const item of PAGES) {
  const page = await miniProgram.reLaunch(item.path)
  await page.waitFor(500)
  await miniProgram.screenshot({
    path: path.join(OUTPUT_DIR, `${item.id}.png`),
  })
}

Additional notes:

  • If the page depends on async requests or animations, do not rely only on waitFor(500); combine it with selector waits
  • It is best to output index.json to summarize whether each page succeeded
  • When the user only wants screenshots, you can also directly generate a dedicated script in this direction

When to give the Jest version

If the user explicitly says "write it as Jest cases", "integrate into CI", or "make a regression suite", rewrite the standalone script as:

  • beforeAll: start miniProgram
  • afterAll: restore Mock, close miniProgram
  • test/it: execute page navigation, interactions, and assertions

But do not switch to Jest by default when the user only wants "a script template". That turns a simple problem into a complex one.

Common mistakes and corrections

Common mistake Correct approach
Write projectPath fixed as the source repository root directory Write it as "the directory actually opened by the devtools"; in scenarios such as Taro it may be dist/
Only remind about installing dependencies, not about security settings You must remind to enable the service port
Rely entirely on fixed sleep Prefer page.waitFor(selector/condition)
Use page.$('form-panel input') to find elements inside a component First page.$('form-panel'), then panel.$('input')
Use a runtime patch wx.request as the default approach Default preference miniProgram.mockWxMethod('request', ...)
forgetting to restore the mock in finally or afterAll in restoreWxMethod
treat screenshots as real-device capability clearly state that screenshots are only supported on the developer tool simulator
directly reading input.data() as a general solution Native input boxes take priority value() / property('value'); read component state from the component instance data()
use miniprogram-automator an old version (0.5.x, etc.) upgrade to 0.12.0+, the old CLI command format is deprecated and will report a code 31 error
automator.launch() called while the developer tool is already open before launch, you must ensure the developer tool is completely quit (Cmd+Q)
launch() no timeout set or timeout too short the first launch + user confirmation of trust takes time; recommended timeout: 120000
page path does not start with / (such as page/component/index) must start with / (/page/component/index), otherwise the path will be incorrectly concatenated
directly take a screenshot of a page that may time out when waited for with a selector (such as the home page list) for complex pages such as the home page, using a fixed wait waitFor(2000) as a fallback is more reliable
still use launch()when the developer tool is already open, or launch() always reports an error switch to CLI v2 + connect() approach: first cli auto --project <path> --auto-port <port> open the port, then automator.connect({ wsEndpoint })
connect() after close() the developer tool was closed connect() in close() mode, launch() only disconnects the ws connection and does not close the tool window; behavior differs from

troubleshooting tips

startup failure / timeout

check first:

  1. miniprogram-automator whether the version is >= 0.12.0(old versions will report code 31)
  2. whether the developer tool hascompletely quit(launch() cannot coexist with an already running instance)
  3. whether the developer tool is installed and logged in
  4. whether the CLI path is correct
  5. whether Security Settings → Service Port is enabled
  6. projectPath whether it is really the directory opened by the developer tool (containing project.config.json)
  7. whether a sufficiently long timeoutis set (recommended for first launch 120000)

if launch() keeps failing (especially when the developer tool is already running), switch to CLI v2 + connect() approach (see Template E):

  • first run cli auto --project <path> --auto-port <port> to enable the automation ws port
  • then use automator.connect({ wsEndpoint: 'ws://127.0.0.1:<port>' }) to connect
  • use detectHttpPort() to detect the current HTTP port, which avoids port conflict errors

page navigation succeeds but elements cannot be selected

check first:

  1. whether the selector is stable
  2. whether the page is still rendering asynchronously
  3. whether component internal nodes are mistakenly being searched as page-level nodes

Mock did not take effect

check first:

  1. Whether the mock is installed before the page's first request
  2. Whether the business layer ultimately really calls the mocked wx method
  3. Whether there is residual contamination from the previous round at the end of the test case

Screenshot failed

Check first:

  1. Whether it is running in the developer tool simulator
  2. Whether the page has stably rendered
  3. Whether the output directory is writable

Practical habits when answering

If the user asks you to directly modify project files, follow this path first:

  1. First state which script file you plan to create or modify
  2. First confirm the path, page, CLI, and target behavior
  3. Then write the file
  4. Finally give the run command and the success criteria

If the user is only asking "how to do it", output the template first; do not arbitrarily create a bunch of files.