跳到正文
cccskills
免费在 GitHub 公开

add-to-homescreen-flow-tests

The single install-flow skill: the reference contract for the PWA install flow (installability gate fields, `beforeinstallprompt` handshake, per-platform paths - Android WebAPK / iOS Share menu / Firefox no-op - and the `display-mode` post-install signal, in references/install-flow-reference.md) plus the build-an-X workflow that emits the Add-to-Home-Screen suite. Walks the four-stage timeline, emitting one test per gate cell per [web.dev/articles/install-criteria][install-criteria], the deferred-prompt → `prompt()` → `userChoice` chain per [web.dev/articles/customize-install][customize-install], the iOS Safari manual-metadata branch (`apple-touch-icon`) per [web.dev/learn/pwa/installation][learn-pwa], and the post-install `(display-mode: standalone)` MQ assertion. Output: a Playwright spec with per-stage cells plus a coverage matrix. Use when a PWA's manifest, icons, or install handler change, when install conversion drops at an unknown stage, or when triaging a flaky install assertion.

中文简介正在准备中。当前显示原文说明。

testland/qa星标 13(GitHub 13 + cccskills 0)
集成工具Playwright

实测评分 未实测

查看安装方法

包含的文件(5)

  • SKILL.md17.2 KB
  • .tessl-plugin/plugin.json1.1 KB
  • references/install-flow-reference.md9.1 KB
  • references/install-flow-tests.md6.6 KB
  • references/install-suite.md3.0 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

add-to-homescreen-flow-tests

Overview

The PWA install flow is a four-stage test surface per references/install-flow-reference.md: installability gate → beforeinstallprompt handshake → per-platform install path → post-install display-mode signal. Every team's install regression looks slightly different - a missing start_url, an icon resolution drift, a prompt() called without a user gesture - but the test surface is the same.

This builder produces the per-PWA install suite. Output is a Playwright spec file plus a coverage YAML matrix mapping each install criterion from install-criteria to its assertion. The contract itself - gate cells, event lifecycle, per-platform paths, and the full event timeline - lives in references/install-flow-reference.md; the workflow consumes it by emitting verification cells.

This builder generates the per-PWA suite from the project's actual manifest + SW + page handler - the artifact you check into the repo. Standalone generic recipes (single-test manifest validation, prompt capture, iOS metadata, display-mode MQ) are in references/install-flow-tests.md.

Composes with:

  • references/install-flow-reference.md - the four-stage timeline this builder follows step-by-step.
  • service-worker-lifecycle-tests - Stage 1 of the install gate requires an active SW; pair this builder's output with the lifecycle spec for full coverage.

When to use

  • New PWA, pre-launch - generate the install suite alongside the manifest.
  • Manifest changed (renamed name, new icon, dropped display field) - re-run the builder to catch criteria regressions.
  • Install conversion is dropping in analytics - author the per-stage cells to localize where users fall out (gate fails → prompt never fires; prompt fires → user dismisses; install succeeds but display-mode flips wrong).
  • Adding iOS support to a Chromium-first PWA - author the manual- metadata branch.

Workflow

Step 1 - Read the manifest + page handler

# Inventory
cat public/manifest.webmanifest > tests/install-snapshot/manifest.json
grep -E "beforeinstallprompt|appinstalled" src/ -rn > tests/install-snapshot/handlers.txt

Capture for the test:

FactUsed in
Manifest fields (name, short_name, display, start_url, icons sizes)Step 2
Whether the page binds beforeinstallpromptStep 3
The selector of the in-app "Install" buttonStep 3
Whether appinstalled is bound (analytics)Step 4
Whether apple-touch-icon + apple-mobile-web-app-capable meta are presentStep 5

Step 2 - Emit the gate-cell tests

Per install-criteria, every gate cell is independently assertable. Emit one test per cell:

// tests/install-gate.spec.ts
import { test, expect } from '@playwright/test';

// Fetch + parse the linked manifest once - reused by every field cell below.
async function readManifest(page, request) {
  const href = await page.locator('link[rel="manifest"]').getAttribute('href');
  return (await request.get(new URL(href!, page.url()).toString())).json();
}

test.describe('PWA install gate (per web.dev/articles/install-criteria)', () => {
  test('manifest link present', async ({ page }) => {
    await page.goto('https://localhost:3000/');
    await expect(page.locator('link[rel="manifest"]')).toHaveCount(1);
  });

  test('manifest declares name or short_name', async ({ page, request }) => {
    await page.goto('https://localhost:3000/');
    const m = await readManifest(page, request);
    expect(m.name || m.short_name).toBeTruthy();
  });

  test('manifest icons include 192px and 512px (per install-criteria)', async ({ page, request }) => {
    await page.goto('https://localhost:3000/');
    const m = await readManifest(page, request);
    const has192 = (m.icons ?? []).some((i: any) => /(^|\s)192x192(\s|$)/.test(i.sizes ?? ''));
    const has512 = (m.icons ?? []).some((i: any) => /(^|\s)512x512(\s|$)/.test(i.sizes ?? ''));
    expect(has192 && has512).toBe(true);
  });

  test('manifest start_url present', async ({ page, request }) => {
    await page.goto('https://localhost:3000/');
    const m = await readManifest(page, request);
    expect(m.start_url).toBeTruthy();
  });

  test('manifest display is installable value', async ({ page, request }) => {
    await page.goto('https://localhost:3000/');
    const m = await readManifest(page, request);
    // Per install-criteria: must be fullscreen, standalone, minimal-ui, or window-controls-overlay
    expect(['fullscreen', 'standalone', 'minimal-ui', 'window-controls-overlay']).toContain(m.display);
  });

  test('manifest does not opt out via prefer_related_applications', async ({ page, request }) => {
    await page.goto('https://localhost:3000/');
    const m = await readManifest(page, request);
    // Per install-criteria: "must not be present or be false"
    expect(m.prefer_related_applications === undefined || m.prefer_related_applications === false).toBe(true);
  });

  test('site served over HTTPS', async ({ page }) => {
    await page.goto('https://localhost:3000/');
    // Allow localhost http for dev; production check enforces https
    const url = page.url();
    expect(url.startsWith('https://') || url.startsWith('http://localhost')).toBe(true);
  });

  test('service worker is registered (install prerequisite)', async ({ page, context }) => {
    await page.goto('https://localhost:3000/');
    let [sw] = context.serviceWorkers();
    if (!sw) sw = await context.waitForEvent('serviceworker');
    expect(sw.url()).toBeTruthy();
  });
});

Step 3 - Emit the beforeinstallprompt handshake test

Per customize-install, the canonical lifecycle is preventDefault → stash → prompt() on user gesture → userChoice. Emit the test:

test('beforeinstallprompt: deferred prompt + click → userChoice resolves', async ({ page }) => {
  await page.goto('https://localhost:3000/');

  // Simulate engagement gate - per install-criteria, "Users must click/tap the page
  // at least once and spend minimum 30 seconds viewing it"
  await page.click('body');
  await page.waitForTimeout(31_000);

  // Wait for the deferred prompt to land on window.__deferredPrompt
  const deferred = await page.waitForFunction(
    () => (window as any).__deferredPrompt !== undefined,
    null,
    { timeout: 10_000 }
  );
  expect(deferred).toBeTruthy();

  // Click the in-app Install button
  await page.click('[data-testid="install-pwa"]');

  // userChoice resolves to { outcome: 'accepted' | 'dismissed' }
  const outcome = await page.evaluate(async () => {
    const p = (window as any).__lastUserChoice;
    return p?.outcome ?? null;
  });
  expect(['accepted', 'dismissed']).toContain(outcome);
});

This requires the page bundle to expose window.__deferredPrompt and window.__lastUserChoice for the test (or use a Playwright init script that hooks the event). The 31-second engagement wait is the engagement-gate cell from install-criteria.

Per customize-install: "You can only call prompt() on the deferred event once." - emit a second-call assertion:

test('beforeinstallprompt: second prompt() call rejects', async ({ page }) => {
  await page.goto('https://localhost:3000/');
  // ... engagement + prompt as above ...

  const error = await page.evaluate(async () => {
    try {
      await (window as any).__deferredPrompt.prompt();
      return null;
    } catch (e: any) {
      return e.message;
    }
  });
  expect(error).not.toBeNull();
});

Step 4 - Emit the appinstalled analytics test

Per customize-install: appinstalled "fires whenever installation succeeds, regardless of the trigger mechanism." Test the listener binds and fires:

test('appinstalled fires after install acceptance', async ({ page }) => {
  await page.goto('https://localhost:3000/');

  const fired = await page.evaluate(() => new Promise<boolean>((resolve) => {
    window.addEventListener('appinstalled', () => resolve(true));
    // Trigger install (test fixture mocks the prompt to auto-accept)
    (window as any).__triggerInstall?.();
    setTimeout(() => resolve(false), 5_000);
  }));
  expect(fired).toBe(true);
});

The Playwright environment does not surface a real install (no WebAPK minting headlessly), so the fixture either (a) mocks the prompt resolver, or (b) dispatches a synthetic appinstalled event in test mode.

Step 5 - Emit the iOS-metadata branch

Per learn-pwa, iOS Safari does not implement beforeinstallprompt. The test surface is metadata + manual smoke:

test('iOS install metadata: apple-touch-icon present', async ({ page }) => {
  await page.goto('https://localhost:3000/');
  await expect(page.locator('link[rel="apple-touch-icon"]')).toHaveCount(1);
});

test('iOS install metadata: apple-mobile-web-app-capable yes', async ({ page }) => {
  await page.goto('https://localhost:3000/');
  await expect(
    page.locator('meta[name="apple-mobile-web-app-capable"][content="yes"]')
  ).toHaveCount(1);
});

test('iOS install metadata: apple-touch-icon resolves', async ({ page, request }) => {
  await page.goto('https://localhost:3000/');
  const href = await page.locator('link[rel="apple-touch-icon"]').getAttribute('href');
  const r = await request.get(new URL(href!, page.url()).toString());
  expect(r.status()).toBe(200);
  // Icon must be PNG for iOS
  expect(r.headers()['content-type']).toMatch(/png/i);
});

Per learn-pwa: iOS install "requires apple-touch-icon tag" - omitting this means installed PWAs get a generic icon, a regression invisible until users file a bug.

Step 6 - Emit the post-install display-mode test

Post-install, the PWA detects its installed state via the display-mode MQ. Playwright doesn't auto-simulate installation; launch with --app= for the standalone path:

import { chromium, expect, test } from '@playwright/test';

// Launch the standalone app context once - shared by both runtime cells.
async function launchInstalled() {
  const ctx = await chromium.launchPersistentContext('./tmp/installed-app', {
    args: ['--app=https://localhost:3000/'],
  });
  const page = await ctx.newPage();
  await page.goto('https://localhost:3000/');
  return { ctx, page };
}

test('display-mode: standalone in launched-as-app context', async () => {
  const { ctx, page } = await launchInstalled();
  const standalone = await page.evaluate(() =>
    matchMedia('(display-mode: standalone)').matches
  );
  expect(standalone).toBe(true);
  await ctx.close();
});

test('display-mode: hides Install button when standalone', async () => {
  const { ctx, page } = await launchInstalled();
  // Per references/install-flow-reference.md Stage 4: apps hide the Install button when already installed
  await expect(page.locator('[data-testid="install-pwa"]')).not.toBeVisible();
  await ctx.close();
});

Step 7 - Emit the coverage matrix

Write tests/install-coverage.yaml, mapping every gate, handshake, iOS-metadata, and runtime cell to its spec and source criterion. The full matrix is in references/install-suite.md; its per-stage shape:

# tests/install-coverage.yaml
matrix:
  stage_1_gate:
    - cell: manifest_link
      spec: install-gate.spec.ts > "manifest link present"
      source: install-criteria
    # ... 7 more gate cells
  stage_2_handshake: # beforeinstallprompt userChoice + second-prompt reject
  stage_2_appinstalled: # appinstalled fires
  stage_3_ios: # apple-touch-icon + apple-mobile-web-app-capable
  stage_4_runtime: # display-mode standalone + install button hidden

CI gates on every matrix cell having a passing spec.

Worked example: a 14-cell install suite

For a PWA with manifest { name, short_name, display: 'standalone', start_url: '/', icons: [192, 512] }, SW at /sw.js, install button [data-testid="install-pwa"], and iOS support:

StageCells emitted
Stage 1 (gate)8 cells (Step 2)
Stage 2 (handshake)2 cells (Step 3)
Stage 2 (analytics)1 cell (Step 4)
Stage 3 (iOS)3 cells (Step 5)
Stage 4 (runtime)2 cells (Step 6)

Total: 16 cells across four spec files. Runs in ~45 seconds (dominated by the 31-second engagement-wait test cell). Catches the four classes of install regression most teams hit:

  1. Manifest field drift breaking the gate.
  2. prompt() called outside a user gesture.
  3. iOS icon omitted, install renders generic.
  4. In-app Install button visible after install (looks broken).

Anti-patterns

Anti-patternWhy it failsFix
One test asserting "install works"Per-cell regressions invisibleOne spec per gate cell (Step 2)
Skip the 30s engagement waitbeforeinstallprompt never fires; test falsely fails on a gate cellStep 3 explicit wait
Test prompt() on page loadBrowser blocks per customize-install; test never reaches userChoiceAlways bind to a user-gesture click
Pin a specific display valuePer install-criteria, four values are installabletoContain not toBe (Step 2)
Mock beforeinstallprompt with dispatchEvent(new Event())Real BeforeInstallPromptEvent has .prompt() + .userChoice methods; synthetic event lacks themHook a real listener in an init script
Skip iOS metadata tests because "we'll add it later"Existing PWAs lose iOS users silently when icon resolves to 404Always include Step 5
Assert display-mode: standalone from a normal Playwright pageA normal page is display-mode: browser; need --app= launchStep 6 launches with --app=
Assume appinstalled fires the same sessionReal installs may fire post-close; test plan must bind earlyBind listener at page load (Step 4)

Limitations

  • The 31-second engagement wait is real. Per install-criteria "Users must click/tap the page at least once and spend minimum 30 seconds viewing it." The test cell from Step 3 sleeps the wait literally - slow tests by ~30s. Run only on release branches if PR-CI time pressure is high.
  • Headless WebAPK minting can't be asserted. Step 4's appinstalled test verifies the event fires; the actual Android WebAPK creation is opaque per references/install-flow-reference.md Stage 3.
  • iOS install is fully manual. Step 5's tests cover the metadata; the actual Share → Add to Home Screen flow needs a real device. Document the manual smoke step in the PR template.
  • --app= launch is Chromium-only. Firefox / WebKit cannot be driven into a display-mode: standalone context from headless Playwright; Step 6 is Chromium-only by design.
  • prefer_related_applications: true suppresses the gate per install-criteria; the Step 2 cell asserts the field is absent or false, but a project that intentionally sets it must skip this cell (and the install flow as a whole).
  • Test fixture needs hooks. Steps 3, 4 require window.__deferredPrompt and window.__triggerInstall to be surfaced by the page bundle in test mode. Document the hooks alongside the spec files.

References

  • web.dev - Install criteria (every gate cell, including HTTPS, SW, manifest fields, and the 30s engagement requirement) - install-criteria.
  • web.dev - Learn PWA: Installation (per-platform flow, iOS Share menu path, apple-touch-icon requirement) - learn-pwa.
  • web.dev - Customize the install experience (beforeinstallprompt handshake, prompt() once-only, appinstalled firing conditions) - customize-install.
  • The install-flow contract (gate cells, handshake, per-platform paths, event timeline) - references/install-flow-reference.md.
  • Composes: service-worker-lifecycle-tests.
  • Generic standalone install-flow test recipes - references/install-flow-tests.md.
  • Sibling builders: offline-fallback-tests, service-worker-lifecycle-tests.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

Builds a CI gate that fails the build on **new** WCAG / a11y violations introduced by a PR while grandfathering pre-existing violations on a per-rule / per-page baseline. Aggregates verdicts from axe-core / pa11y / Lighthouse a11y / WAVE / IBM Equal Access scans. Use when a project has accumulated a11y debt and a strict "zero violations" gate would block every PR - the ratchet pattern lets the team ship while preventing regressions.

中文简介正在准备中。当前显示原文说明。

testland/qa132026年10月20日 更新

Workflow skill that builds an A/B-test validity checklist from an experiment proposal, walking the canonical design-correctness gates - pre-registered OEC/power/guardrails, randomization unit + SRM check, assignment integrity, telemetry, peeking discipline, novelty/primacy, post-experiment SRM re-check - into a per-experiment checklist + sign-off form. Use when launching, auditing, or governing an experiment. For pitfall mechanics (guardrails, peeking) see experiment-results-interpreter's references; to read an already-valid result use experiment-results-interpreter; for per-SDK harness tests use experiment-sdk-testing - this gates DESIGN, not SDK code.

中文简介正在准备中。当前显示原文说明。

testland/qa132026年10月20日 更新

Build-an-X workflow that uses an LLM to map existing tests to spec sections - given a spec doc + the test suite, the LLM identifies which tests cover which sections, surfaces uncovered sections (gap), and recommends specific tests to add. Output is a coverage matrix per spec ID. Scope is mapping tests that already exist and naming the gaps, not authoring tests for new acceptance criteria. Use when a spec doc and a test suite both exist but nobody can say which requirements are actually covered - before a release sign-off, an audit, or a decision about where to spend the next round of test effort.

中文简介正在准备中。当前显示原文说明。

testland/qa132026年10月20日 更新

Generates tests from natural-language specs (acceptance criteria, user stories, requirements) using an LLM, with confidence scoring per test case (LLM self-assessment plus heuristics: assertion quality, naming, completeness), batching uncertain cases for human review, and integration with the team's existing test framework. Use when the user asks to generate unit tests from acceptance criteria, convert user stories to test cases, automate test creation from requirements, or augment a spec-driven test suite with AI-generated stubs that are then curated before merge.

中文简介正在准备中。当前显示原文说明。

testland/qa132026年10月20日 更新

Configures Allure Report (test-runner adapter install, `allure-results` directory wiring, `categories.json` for failure classification, `history-trend.json` retention via the copy-history-between-runs pattern), runs the Allure CLI to convert `allure-results` to a static HTML site, and uploads the report as a CI artifact. Use when the team needs richer test reporting than JUnit XML - step-level attachments, per-test history, retry tracking, and severity / epic / feature labeling across framework-agnostic adapters (pytest, Jest, JUnit, TestNG, NUnit, Mocha). As a rich static HTML report generator, it is the open-source alternative to the sunset ExtentReports (JVM/.NET per-test HTML narrative); for hosted cross-run flakiness analytics rather than a static per-run report use currents-integration.

中文简介正在准备中。当前显示原文说明。

testland/qa132026年10月20日 更新

Runs the project's existing API tests under injected network chaos - latency, timeouts, dropped connections, bandwidth caps, packet loss - via Toxiproxy (notes on Pumba / Gremlin / LitmusChaos). Builds a per-scenario chaos matrix and reports which assertions break under which conditions, verifying resilience patterns (retry, circuit-breaker, timeout, fallback). Unlike schemathesis-fuzzing and restler-fuzzing, which generate new tests from a schema, this drives your EXISTING example-based suite.

中文简介正在准备中。当前显示原文说明。

testland/qa132026年10月20日 更新

查看 testland 的全部技能

报告此技能的问题