[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71P2ACYDBKDNR30Y9HQPFJJ":3,"skill-same:01M71P2ACYDBKDNR30Y9HQPFJJ":96},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":49,"linkOnly":50,"markdownContent":51,"markdownBody":52,"frontMatter":53,"refName":57,"commitSha":58,"skillDirectory":59,"forksCount":14,"repositoryHtmlUrl":60,"repositoryHomepageUrl":61,"ownerHtmlUrl":62,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":63,"install":64,"resources":77},"01M71P2ACYDBKDNR30Y9HQPFJJ","payment-testing",null,"Test payment and checkout flows end to end against PSP sandboxes — Stripe first, with the general pattern for Adyen\u002FBraintree\u002FPayPal. Covers Stripe test-mode card numbers and their decline codes, the 3DS\u002FSCA challenge flow and its nested-iframe handling in Playwright, test clocks for subscription\u002Fbilling-cycle simulation, webhook testing (stripe listen\u002Ftrigger, signature verification, idempotency), failed\u002Fretried payments and refunds, and never using real cards. Use when: \"test Stripe checkout,\" \"payment test,\" \"3DS test,\" \"test webhook signature,\" \"test subscription renewal,\" \"test clock,\" \"refund test,\" \"decline card test,\" \"checkout E2E.\" Not for: General API contract testing of non-payment endpoints — api-testing. PCI-DSS\u002Fregulatory compliance audit — compliance-testing. Related: api-testing, playwright-automation, compliance-testing, test-data-management, qa-project-context.","pattanation\u002FNorvik","pattanation","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F235419268?v=4","01M71M5THHYYB15ME8R2E4FCZG","Patta","01M71M5TJM1NDM992M8PDKYR2K",0,"MIT","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fpayment-testing\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48],"payment","payment testing","testing","clock","use","paypal","general","api contract","for","refund","context","subscription","pci","simulation","specialized","challenge","mode","card","project","webhook","2026-11-10 10:56:23",false,"---\nname: payment-testing\ndescription: >-\n  Test payment and checkout flows end to end against PSP sandboxes — Stripe first, with\n  the general pattern for Adyen\u002FBraintree\u002FPayPal. Covers Stripe test-mode card numbers and\n  their decline codes, the 3DS\u002FSCA challenge flow and its nested-iframe handling in\n  Playwright, test clocks for subscription\u002Fbilling-cycle simulation, webhook testing\n  (stripe listen\u002Ftrigger, signature verification, idempotency), failed\u002Fretried payments and\n  refunds, and never using real cards. Use when: \"test Stripe checkout,\" \"payment test,\"\n  \"3DS test,\" \"test webhook signature,\" \"test subscription renewal,\" \"test clock,\"\n  \"refund test,\" \"decline card test,\" \"checkout E2E.\"\n  Not for: General API contract testing of non-payment endpoints — api-testing. PCI-DSS\u002Fregulatory\n  compliance audit — compliance-testing.\n  Related: api-testing, playwright-automation, compliance-testing, test-data-management, qa-project-context.\nlicense: MIT\nmetadata:\n  author: kindlmann\n  version: \"1.0\"\n  category: specialized\n---\n\n\u003Cobjective>\nPayment flows fail in ways generic E2E tests miss: a card field in a cross-origin iframe\nthat `page.locator` silently never reaches, a renewal that won't trigger for a year, a\nwebhook handler that \"works\" until Stripe retries and fulfills an order twice, an order\nmarked paid on a redirect that the browser forged. This skill makes you test payments the\nway they actually break — against PSP sandboxes, with the real test cards, the nested 3DS\nchallenge, server-side test clocks, signature-verified webhooks, and idempotent\nfulfillment. Never a real PAN, never a live key.\n\u003C\u002Fobjective>\n\n## Quick Route\n\n| You need to test… | Go to | Reference |\n|-------------------|-------|-----------|\n| Success \u002F decline \u002F insufficient-funds outcomes | [Test cards](#stripe-test-cards-and-outcomes) | `references\u002Fstripe-test-cards.md` |\n| A 3DS\u002FSCA challenge that pops a modal | [3DS challenge](#3ds--sca-the-nested-iframe-challenge) | `references\u002Fplaywright-3ds.md` |\n| A subscription renewal months\u002Fyears out | [Test clocks](#test-clocks-server-side-time-travel) | `references\u002Fwebhooks-and-clocks.md` |\n| Webhooks reaching localhost + signatures | [Webhooks](#webhooks-local-delivery-signatures-idempotency) | `references\u002Fwebhooks-and-clocks.md` |\n| A failed renewal then a refund | [Failed payments](#failed-payments-dunning-and-refunds) | `references\u002Fwebhooks-and-clocks.md` |\n| Fulfillment only after real payment | [Reconciliation](#reconciliation-fulfill-on-the-webhook-not-the-redirect) | `references\u002Fwebhooks-and-clocks.md` |\n| Adyen \u002F PayPal \u002F Braintree sandboxes | [Multi-PSP](#multi-psp-adyen-paypal-braintree) | `references\u002Fmulti-psp.md` |\n\n## Discovery Questions\n\nFirst, check `.agents\u002Fqa-project-context.md` in the project root and skip anything it\nalready answers (PSP, stack, test framework, existing fixtures). Then clarify:\n\n- **Which PSP, and is it Stripe?** Stripe is the default here. Other PSPs share the pattern\n  but have their own sandbox cards\u002Faccounts — see [Multi-PSP](#multi-psp-adyen-paypal-braintree).\n- **One-time payments, subscriptions, or both?** Subscriptions pull in test clocks, dunning,\n  and the `invoice.*` lifecycle; one-time payments don't.\n- **Is SCA\u002F3DS in play?** EU\u002FUK card flows almost always challenge. If yes you need the\n  nested-iframe pattern, not plain locators.\n- **Do you fulfill on a webhook or on the redirect today?** If fulfillment happens on\n  `return_url`, that's the bug to test for — fulfillment must wait for the verified webhook.\n- **Where does the webhook handler run in tests?** Local (`stripe listen`) vs a deployed\n  preview env changes how you deliver events.\n\n## Core Principles\n\n1. **Never touch a real card or a live key — and this is non-negotiable, not a preference.**\n   Real PANs in any environment violate Stripe's Services Agreement and drag your repo into\n   PCI scope. Test mode (`pk_test_`\u002F`sk_test_`) with Stripe's published test cards is the\n   only correct answer. Masking or encrypting a real number does not fix it; removing it\n   does.\n\n2. **Money is confirmed server-side, never client-side.** A redirect, an `onApprove`\n   callback, or a `?status=success` query param can be premature, replayed, or forged.\n   Fulfill an order only after a signature-verified `payment_intent.succeeded` webhook,\n   re-confirmed with an API `retrieve`.\n\n3. **Verify the signature before you parse the body.** Parsing JSON first destroys the raw\n   bytes `constructEvent` needs. The webhook route gets the raw body; everything else can\n   parse JSON.\n\n4. **Time is a server-side construct for billing.** Stripe's billing engine runs on\n   Stripe's servers. Faking the clock in your test process changes nothing. Use test clocks.\n\n5. **Assume every webhook is delivered more than once.** Stripe retries. Idempotency keyed\n   on `event.id` in durable storage is mandatory; an in-memory set is not idempotency.\n\n## Stripe Test Cards and Outcomes\n\nDrive each outcome with the card that deterministically produces it. The four you need most:\n\n| PAN | Outcome | Code |\n|-----|---------|------|\n| `4242424242424242` | Succeeds | — |\n| `4000000000000002` | Declined | `card_declined` (generic_decline) |\n| `4000000000009995` | Declined | `insufficient_funds` |\n| `4000000000003220` | 3DS always challenges | — |\n| `4000000000000341` | Attaches, then fails on charge | `card_declined` |\n\nUse test keys (`pk_test_…`\u002F`sk_test_…`) in the app under test and assert that in setup.\n**Do not use `4111111111111111`** — that is a generic Braintree\u002FPayPal-era Luhn number,\nnot a Stripe test card, and it does not deterministically decline.\n\nThe card field is in a cross-origin Stripe iframe, so fill it through `frameLocator`, never\n`page.locator` directly. Assert outcomes on UI copy for a smoke test, or more robustly on\nthe server-side `last_payment_error.decline_code` from `paymentIntents.retrieve`. Full\nPlaywright tests for success \u002F `card_declined` \u002F `insufficient_funds`:\n`references\u002Fstripe-test-cards.md`.\n\n## 3DS \u002F SCA: The Nested-Iframe Challenge\n\nThis is the hardest part to get right. The Stripe 3DS challenge is **a frame nested inside\nthe Stripe modal frame** — a single `frameLocator` cannot reach it. Chain\n`frameLocator` outer → inner, then click **Complete authentication**.\n\nWhat fails, and why:\n\n- `page.locator('#card')` → the input is cross-origin; the locator matches nothing.\n- `page.frames()[1]` → frame **index** shifts when Stripe adds\u002Freorders frames. Never select\n  frames by index.\n- `await page.waitForTimeout(5000)` → guessing the challenge duration. Wait on the element.\n\nThe correct shape (full test, including the fail-authentication variant, in\n`references\u002Fplaywright-3ds.md`):\n\n```ts\n\u002F\u002F 3DS-required card so the challenge always appears.\nawait card.getByPlaceholder('Card number').fill('4000000000003220');\nawait page.getByRole('button', { name: \u002Fpay\u002Fi }).click();\n\n\u002F\u002F Nested: outer Stripe challenge frame → inner ACS frame. One frameLocator is not enough.\nconst inner = page\n  .frameLocator('iframe[name^=\"__privateStripeFrame\"]')\n  .frameLocator('iframe#challengeFrame, iframe[name=\"acsFrame\"]');\nawait inner.getByRole('button', { name: \u002Fcomplete authentication|complete|authorize\u002Fi }).click();\n\nawait expect(page).toHaveURL(\u002F\\\u002Fsuccess\u002F);              \u002F\u002F assert the succeeded state\nawait expect(page.getByText(\u002Fpayment succeeded\u002Fi)).toBeVisible();\n```\n\n`4000002760003184` is the alternative SCA card for setup-intent \u002F first-use flows; the eval\nand docs accept it where a one-time-payment 3DS card is wanted.\n\n## Test Clocks: Server-Side Time Travel\n\nTo test an annual renewal without waiting a year, use a Stripe **test clock** — a\nserver-side construct. Client-side fakes (`jest.useFakeTimers`, sinon, mocking `Date`) do\nnothing to Stripe's billing engine.\n\nRules that bite if missed:\n- Create the clock at a `frozen_time`, then attach the **customer at creation** with\n  `test_clock: clock.id`. You cannot attach an existing customer to a clock afterward.\n- `testHelpers.testClocks.advance` moves time **forward only** — you cannot rewind. Advance\n  at most two billing cycles per call.\n- After advancing, poll the clock to `ready`, then assert the renewal invoice and webhooks.\n\n```ts\nconst clock = await stripe.testHelpers.testClocks.create({\n  frozen_time: Math.floor(Date.now() \u002F 1000), name: 'annual-renewal',\n});\nconst customer = await stripe.customers.create({ test_clock: clock.id \u002F* … *\u002F });\n\u002F\u002F …create subscription, then advance ~12 months forward:\nawait stripe.testHelpers.testClocks.advance(clock.id, { frozen_time: oneYearLater });\n```\n\nFull create\u002Fadvance\u002Fassert flow: `references\u002Fwebhooks-and-clocks.md` (section 4).\n\n## Webhooks: Local Delivery, Signatures, Idempotency\n\n**Local delivery.** Do not expose your endpoint with ngrok and do not poll the API for\nstatus. `stripe listen` tunnels test events to localhost natively; `stripe trigger` fires\nthem on demand:\n\n```bash\nstripe listen --forward-to localhost:3000\u002Fwebhooks   # prints whsec_… ONCE at startup\nstripe trigger payment_intent.succeeded\n```\n\nCopy that `whsec_…` into `STRIPE_WEBHOOK_SECRET`. It is the **signing secret**, a different\nvalue from `STRIPE_SECRET_KEY` (`sk_test_…`) — do not conflate them.\n\n**Signature verification.** Mount `express.raw` on the webhook route **before** any global\n`express.json()`, so `constructEvent` gets the raw body. A forged or tampered event must be\nrejected with **400**; never hand-roll a `=== signature` string comparison.\n\n```ts\napp.post('\u002Fwebhooks', express.raw({ type: 'application\u002Fjson' }), (req, res) => {\n  const sig = req.headers['stripe-signature'] as string;\n  try {\n    const event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);\n    return handleEvent(event, res);\n  } catch (err) {\n    return res.status(400).send(`Webhook Error: ${(err as Error).message}`); \u002F\u002F SignatureVerificationError\n  }\n});\napp.use(express.json()); \u002F\u002F everything else, after the webhook route\n```\n\n**Idempotency.** Stripe retries delivery, so the same `event.id` arrives twice. Request-side\nidempotency keys (for outbound API calls) do not dedup inbound webhooks. Store `event.id`\nwith a **UNIQUE** constraint and short-circuit on conflict; an in-memory set is lost on\nrestart and useless across instances. The handler returns **200** for a duplicate so Stripe\nstops retrying, and fulfillment runs exactly **once**.\n\n```ts\nconst inserted = await db.query(\n  `INSERT INTO processed_events (id) VALUES ($1) ON CONFLICT (id) DO NOTHING RETURNING id`, [event.id]);\nif (inserted.rowCount === 0) return res.status(200).send('duplicate ignored');\n```\n\nThe signature test (valid accepted, forged → 400) and the \"deliver the same event twice,\nassert fulfilled once\" idempotency test are in `references\u002Fwebhooks-and-clocks.md`\n(sections 2–3).\n\n## Failed Payments: Dunning and Refunds\n\nTo test a failed recurring charge end to end, subscribe with `4000000000000341` (SDK token\n`pm_card_chargeCustomerFail`) — it **attaches** to the customer but **fails on the later\ncharge**, which is what the renewal needs. Cards that decline at attach time can't be saved,\nso they can't model a renewal failure.\n\nDrive the lifecycle with a test clock:\n1. Subscribe the customer (on a test clock) with the attach-then-fail card.\n2. `advance` the clock past the renewal date → Stripe attempts the charge.\n3. The charge fails → Stripe emits **`invoice.payment_failed`** and the subscription goes\n   **`past_due`**. Assert both.\n4. Resolve by issuing a refund with **`refunds.create`** (fires `charge.refunded`) — do\n   **not** \"fix\" it by deleting the subscription.\n\nFull driver in `references\u002Fwebhooks-and-clocks.md` (section 5).\n\n## Reconciliation: Fulfill on the Webhook, Not the Redirect\n\nMark an order paid **only** after a signature-verified `payment_intent.succeeded` webhook,\nre-confirmed against the API — never on the `return_url` redirect or a client-side success\nflag, and never by polling with a sleep.\n\n```ts\nif (event.type === 'payment_intent.succeeded') {\n  const verified = await stripe.paymentIntents.retrieve(event.data.object.id);\n  if (verified.status === 'succeeded' && verified.amount_received === expected) {\n    await markOrderPaid(verified.metadata.orderId); \u002F\u002F fulfillment happens HERE\n  }\n}\n```\n\nThe reconciliation test asserts the order is still `pending` after the redirect and only\n`paid` after the verified webhook: `references\u002Fwebhooks-and-clocks.md` (section 6).\n\n## Multi-PSP: Adyen, PayPal, Braintree\n\nStripe test cards do **not** work on other PSPs. Each has its own sandbox cards and sandbox\nbuyer accounts. Port the *structure* of your Stripe tests; swap in the PSP's sandbox values.\nNever reuse Stripe PANs or live\u002Fproduction keys.\n\n- **Adyen** — own test cards (e.g. `4212345678910014` for 3DS2); many declines are driven by\n  the **transaction amount** (`.13` refused, `.51` referral), not the card. Events arrive as\n  HMAC-signed notifications.\n- **PayPal** — log in with a **sandbox buyer account** (sandbox personal email\u002Fpassword), not\n  a card. Confirm server-side via the Orders API \u002F webhooks, not the client `onApprove`.\n- **Braintree** — own sandbox **test card** numbers via Drop-in UI \u002F Hosted Fields; amount\n  drives transaction outcome, card number drives verification.\n\nWhat stays the same: separate test\u002Fsandbox credentials, no real card, and fulfillment on the\nverified server-side event\u002Fnotification. Details: `references\u002Fmulti-psp.md`.\n\n## Anti-Patterns\n\n### 1. Reaching for `4111111111111111`\nThat Luhn-valid number is a Braintree\u002FPayPal-era generic PAN, not a Stripe test card. Use\n`4242424242424242` for success and the specific decline cards (`4000000000000002`,\n`4000000000009995`).\n\n### 2. Treating the card field as a normal input\n`page.locator('#card-number')` silently matches nothing because the field is in a\ncross-origin iframe. Use `frameLocator`.\n\n### 3. Selecting iframes by index\n`page.frames()[1]` breaks the instant Stripe reorders frames. Match the frame by a stable\nname prefix (`iframe[name^=\"__privateStripeFrame\"]`) and chain `frameLocator` for the nested\n3DS challenge.\n\n### 4. `waitForTimeout` to \"wait for the challenge\"\nFlaky on slow CI, wasteful on fast CI. Wait on the element (`expect(...).toBeVisible()` \u002F\nauto-waiting locator actions), never the clock.\n\n### 5. Client-side time mocking for billing\n`jest.useFakeTimers` \u002F sinon \u002F mocking `Date` cannot move Stripe's server-side billing.\nUse a test clock.\n\n### 6. ngrok or polling for local webhooks\n`stripe listen --forward-to localhost:3000\u002Fwebhooks` tunnels events natively; `stripe\ntrigger` fires them. No public tunnel, no status polling.\n\n### 7. Parsing the body before verifying the signature\nA global `express.json()` ahead of the webhook route destroys the raw body\n`constructEvent` needs, so verification can never pass. Mount `express.raw` on the webhook\nroute first.\n\n### 8. Confusing the request idempotency key with webhook dedup, or using an in-memory set\nOutbound idempotency keys don't dedup inbound webhooks; an in-memory `Set` dies on restart.\nPersist `event.id` with a UNIQUE constraint.\n\n### 9. Fulfilling on the redirect \u002F client success flag\nThe `return_url` can be premature, replayed, or forged. Fulfill only on the verified\n`payment_intent.succeeded` webhook.\n\n### 10. \"Fixing\" a failed renewal by deleting the subscription\nThe correct resolution is a refund via `refunds.create`, leaving the dunning lifecycle\n(`invoice.payment_failed` → `past_due`) intact and testable.\n\n### 11. Rationalizing a real card \"just in CI\"\nA hardcoded real PAN is a PCI\u002Fcompliance violation regardless of environment. The fix is a\ntest card in test mode — plus removing the secret from the repo and git history and rotating\nany exposed key. Masking or encrypting it does not make it acceptable.\n\n## Verification\n\n- `stripe listen --forward-to localhost:3000\u002Fwebhooks` prints a `whsec_…` and shows events\n  arriving when you run `stripe trigger payment_intent.succeeded`.\n- Running the 3DS test with `4000000000003220` reaches and clicks the **Complete\n  authentication** button (the test fails loudly, not silently, if the nested frame isn't\n  found).\n- The signature test: a tampered `Stripe-Signature` returns **400**; a header from\n  `generateTestHeaderString` returns **200**.\n- The idempotency test: delivering the same `event.id` twice fulfills **once**.\n- `grep -rE 'pk_live|sk_live|4111111111111111'` over the test suite returns nothing.\n\n## Done When\n\n- Checkout suite covers success (`4242424242424242`), `card_declined`\n  (`4000000000000002`), and `insufficient_funds` (`4000000000009995`), each asserting the\n  matching outcome, using `pk_test_`\u002F`sk_test_` keys.\n- A 3DS test fills `4000000000003220`, reaches the nested challenge frame via chained\n  `frameLocator`, clicks **Complete authentication**, and asserts the succeeded state — no\n  `frames()[index]`, no `waitForTimeout`.\n- A subscription-renewal test uses a Stripe test clock (`testHelpers.testClocks.create` +\n  `advance`, forward-only, customer attached at creation) instead of any client-side time mock.\n- Local webhooks are received via `stripe listen --forward-to` \u002F `stripe trigger`, with the\n  `whsec_…` wired into `STRIPE_WEBHOOK_SECRET` (distinct from `STRIPE_SECRET_KEY`).\n- The webhook handler verifies the signature with `constructEvent` on the **raw** body\n  before parsing, returns 400 on a forged event, and a test proves it.\n- Idempotency is enforced by persisting `event.id` with a UNIQUE constraint; a duplicate\n  delivery returns 200 and fulfills exactly once, proven by a test.\n- A failed-renewal test drives `invoice.payment_failed` → `past_due` → `refunds.create` via\n  a test clock and the attach-then-fail card `4000000000000341`.\n- A reconciliation test confirms the order is `paid` only after the verified\n  `payment_intent.succeeded` webhook (re-checked with `paymentIntents.retrieve`), not on the\n  redirect.\n- `grep -rE 'pk_live|sk_live|4111111111111111'` finds no live key or banned PAN in the test\n  suite. (A bare `[0-9]{16}` scan would false-positive on every legitimate test card —\n  match live-key prefixes and the banned `4111…` number, not all 16-digit strings.)\n\n## Related Skills\n\n- **api-testing** — General REST\u002FGraphQL endpoint testing, schema validation, and auth\n  flows for non-payment endpoints. Go there when the target isn't a PSP checkout\u002Fwebhook.\n- **playwright-automation** — Page Object Model, fixtures, and general browser E2E\n  mechanics that the 3DS flow here builds on.\n- **compliance-testing** — PCI-DSS, GDPR, and regulatory audit work. This skill keeps you\n  *out* of PCI scope by using test cards; go there for a formal compliance audit.\n- **test-data-management** — Seeding customers, subscriptions, and fixtures; managing the\n  test-clock-bound customers this skill creates.\n- **qa-project-context** — The universal first stop: PSP, stack, and fixture conventions\n  that every question above should defer to.\n\n## Reference Files (in `references\u002F`)\n\n- **stripe-test-cards.md** — Full test-card catalogue with decline codes and the Playwright\n  success\u002Fdecline\u002Finsufficient-funds tests.\n- **playwright-3ds.md** — Nested-iframe 3DS challenge handling, complete and fail variants,\n  and selector notes.\n- **webhooks-and-clocks.md** — `stripe listen`\u002F`trigger`, raw-body signature verification,\n  idempotency by `event.id`, test clocks, failed-renewal dunning + refunds, and\n  reconciliation.\n- **multi-psp.md** — Adyen, PayPal, and Braintree sandbox patterns and what differs from\n  Stripe.\n","\u003Cobjective>\nPayment flows fail in ways generic E2E tests miss: a card field in a cross-origin iframe\nthat `page.locator` silently never reaches, a renewal that won't trigger for a year, a\nwebhook handler that \"works\" until Stripe retries and fulfills an order twice, an order\nmarked paid on a redirect that the browser forged. This skill makes you test payments the\nway they actually break — against PSP sandboxes, with the real test cards, the nested 3DS\nchallenge, server-side test clocks, signature-verified webhooks, and idempotent\nfulfillment. Never a real PAN, never a live key.\n\u003C\u002Fobjective>\n\n## Quick Route\n\n| You need to test… | Go to | Reference |\n|-------------------|-------|-----------|\n| Success \u002F decline \u002F insufficient-funds outcomes | [Test cards](#stripe-test-cards-and-outcomes) | `references\u002Fstripe-test-cards.md` |\n| A 3DS\u002FSCA challenge that pops a modal | [3DS challenge](#3ds--sca-the-nested-iframe-challenge) | `references\u002Fplaywright-3ds.md` |\n| A subscription renewal months\u002Fyears out | [Test clocks](#test-clocks-server-side-time-travel) | `references\u002Fwebhooks-and-clocks.md` |\n| Webhooks reaching localhost + signatures | [Webhooks](#webhooks-local-delivery-signatures-idempotency) | `references\u002Fwebhooks-and-clocks.md` |\n| A failed renewal then a refund | [Failed payments](#failed-payments-dunning-and-refunds) | `references\u002Fwebhooks-and-clocks.md` |\n| Fulfillment only after real payment | [Reconciliation](#reconciliation-fulfill-on-the-webhook-not-the-redirect) | `references\u002Fwebhooks-and-clocks.md` |\n| Adyen \u002F PayPal \u002F Braintree sandboxes | [Multi-PSP](#multi-psp-adyen-paypal-braintree) | `references\u002Fmulti-psp.md` |\n\n## Discovery Questions\n\nFirst, check `.agents\u002Fqa-project-context.md` in the project root and skip anything it\nalready answers (PSP, stack, test framework, existing fixtures). Then clarify:\n\n- **Which PSP, and is it Stripe?** Stripe is the default here. Other PSPs share the pattern\n  but have their own sandbox cards\u002Faccounts — see [Multi-PSP](#multi-psp-adyen-paypal-braintree).\n- **One-time payments, subscriptions, or both?** Subscriptions pull in test clocks, dunning,\n  and the `invoice.*` lifecycle; one-time payments don't.\n- **Is SCA\u002F3DS in play?** EU\u002FUK card flows almost always challenge. If yes you need the\n  nested-iframe pattern, not plain locators.\n- **Do you fulfill on a webhook or on the redirect today?** If fulfillment happens on\n  `return_url`, that's the bug to test for — fulfillment must wait for the verified webhook.\n- **Where does the webhook handler run in tests?** Local (`stripe listen`) vs a deployed\n  preview env changes how you deliver events.\n\n## Core Principles\n\n1. **Never touch a real card or a live key — and this is non-negotiable, not a preference.**\n   Real PANs in any environment violate Stripe's Services Agreement and drag your repo into\n   PCI scope. Test mode (`pk_test_`\u002F`sk_test_`) with Stripe's published test cards is the\n   only correct answer. Masking or encrypting a real number does not fix it; removing it\n   does.\n\n2. **Money is confirmed server-side, never client-side.** A redirect, an `onApprove`\n   callback, or a `?status=success` query param can be premature, replayed, or forged.\n   Fulfill an order only after a signature-verified `payment_intent.succeeded` webhook,\n   re-confirmed with an API `retrieve`.\n\n3. **Verify the signature before you parse the body.** Parsing JSON first destroys the raw\n   bytes `constructEvent` needs. The webhook route gets the raw body; everything else can\n   parse JSON.\n\n4. **Time is a server-side construct for billing.** Stripe's billing engine runs on\n   Stripe's servers. Faking the clock in your test process changes nothing. Use test clocks.\n\n5. **Assume every webhook is delivered more than once.** Stripe retries. Idempotency keyed\n   on `event.id` in durable storage is mandatory; an in-memory set is not idempotency.\n\n## Stripe Test Cards and Outcomes\n\nDrive each outcome with the card that deterministically produces it. The four you need most:\n\n| PAN | Outcome | Code |\n|-----|---------|------|\n| `4242424242424242` | Succeeds | — |\n| `4000000000000002` | Declined | `card_declined` (generic_decline) |\n| `4000000000009995` | Declined | `insufficient_funds` |\n| `4000000000003220` | 3DS always challenges | — |\n| `4000000000000341` | Attaches, then fails on charge | `card_declined` |\n\nUse test keys (`pk_test_…`\u002F`sk_test_…`) in the app under test and assert that in setup.\n**Do not use `4111111111111111`** — that is a generic Braintree\u002FPayPal-era Luhn number,\nnot a Stripe test card, and it does not deterministically decline.\n\nThe card field is in a cross-origin Stripe iframe, so fill it through `frameLocator`, never\n`page.locator` directly. Assert outcomes on UI copy for a smoke test, or more robustly on\nthe server-side `last_payment_error.decline_code` from `paymentIntents.retrieve`. Full\nPlaywright tests for success \u002F `card_declined` \u002F `insufficient_funds`:\n`references\u002Fstripe-test-cards.md`.\n\n## 3DS \u002F SCA: The Nested-Iframe Challenge\n\nThis is the hardest part to get right. The Stripe 3DS challenge is **a frame nested inside\nthe Stripe modal frame** — a single `frameLocator` cannot reach it. Chain\n`frameLocator` outer → inner, then click **Complete authentication**.\n\nWhat fails, and why:\n\n- `page.locator('#card')` → the input is cross-origin; the locator matches nothing.\n- `page.frames()[1]` → frame **index** shifts when Stripe adds\u002Freorders frames. Never select\n  frames by index.\n- `await page.waitForTimeout(5000)` → guessing the challenge duration. Wait on the element.\n\nThe correct shape (full test, including the fail-authentication variant, in\n`references\u002Fplaywright-3ds.md`):\n\n```ts\n\u002F\u002F 3DS-required card so the challenge always appears.\nawait card.getByPlaceholder('Card number').fill('4000000000003220');\nawait page.getByRole('button', { name: \u002Fpay\u002Fi }).click();\n\n\u002F\u002F Nested: outer Stripe challenge frame → inner ACS frame. One frameLocator is not enough.\nconst inner = page\n  .frameLocator('iframe[name^=\"__privateStripeFrame\"]')\n  .frameLocator('iframe#challengeFrame, iframe[name=\"acsFrame\"]');\nawait inner.getByRole('button', { name: \u002Fcomplete authentication|complete|authorize\u002Fi }).click();\n\nawait expect(page).toHaveURL(\u002F\\\u002Fsuccess\u002F);              \u002F\u002F assert the succeeded state\nawait expect(page.getByText(\u002Fpayment succeeded\u002Fi)).toBeVisible();\n```\n\n`4000002760003184` is the alternative SCA card for setup-intent \u002F first-use flows; the eval\nand docs accept it where a one-time-payment 3DS card is wanted.\n\n## Test Clocks: Server-Side Time Travel\n\nTo test an annual renewal without waiting a year, use a Stripe **test clock** — a\nserver-side construct. Client-side fakes (`jest.useFakeTimers`, sinon, mocking `Date`) do\nnothing to Stripe's billing engine.\n\nRules that bite if missed:\n- Create the clock at a `frozen_time`, then attach the **customer at creation** with\n  `test_clock: clock.id`. You cannot attach an existing customer to a clock afterward.\n- `testHelpers.testClocks.advance` moves time **forward only** — you cannot rewind. Advance\n  at most two billing cycles per call.\n- After advancing, poll the clock to `ready`, then assert the renewal invoice and webhooks.\n\n```ts\nconst clock = await stripe.testHelpers.testClocks.create({\n  frozen_time: Math.floor(Date.now() \u002F 1000), name: 'annual-renewal',\n});\nconst customer = await stripe.customers.create({ test_clock: clock.id \u002F* … *\u002F });\n\u002F\u002F …create subscription, then advance ~12 months forward:\nawait stripe.testHelpers.testClocks.advance(clock.id, { frozen_time: oneYearLater });\n```\n\nFull create\u002Fadvance\u002Fassert flow: `references\u002Fwebhooks-and-clocks.md` (section 4).\n\n## Webhooks: Local Delivery, Signatures, Idempotency\n\n**Local delivery.** Do not expose your endpoint with ngrok and do not poll the API for\nstatus. `stripe listen` tunnels test events to localhost natively; `stripe trigger` fires\nthem on demand:\n\n```bash\nstripe listen --forward-to localhost:3000\u002Fwebhooks   # prints whsec_… ONCE at startup\nstripe trigger payment_intent.succeeded\n```\n\nCopy that `whsec_…` into `STRIPE_WEBHOOK_SECRET`. It is the **signing secret**, a different\nvalue from `STRIPE_SECRET_KEY` (`sk_test_…`) — do not conflate them.\n\n**Signature verification.** Mount `express.raw` on the webhook route **before** any global\n`express.json()`, so `constructEvent` gets the raw body. A forged or tampered event must be\nrejected with **400**; never hand-roll a `=== signature` string comparison.\n\n```ts\napp.post('\u002Fwebhooks', express.raw({ type: 'application\u002Fjson' }), (req, res) => {\n  const sig = req.headers['stripe-signature'] as string;\n  try {\n    const event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET!);\n    return handleEvent(event, res);\n  } catch (err) {\n    return res.status(400).send(`Webhook Error: ${(err as Error).message}`); \u002F\u002F SignatureVerificationError\n  }\n});\napp.use(express.json()); \u002F\u002F everything else, after the webhook route\n```\n\n**Idempotency.** Stripe retries delivery, so the same `event.id` arrives twice. Request-side\nidempotency keys (for outbound API calls) do not dedup inbound webhooks. Store `event.id`\nwith a **UNIQUE** constraint and short-circuit on conflict; an in-memory set is lost on\nrestart and useless across instances. The handler returns **200** for a duplicate so Stripe\nstops retrying, and fulfillment runs exactly **once**.\n\n```ts\nconst inserted = await db.query(\n  `INSERT INTO processed_events (id) VALUES ($1) ON CONFLICT (id) DO NOTHING RETURNING id`, [event.id]);\nif (inserted.rowCount === 0) return res.status(200).send('duplicate ignored');\n```\n\nThe signature test (valid accepted, forged → 400) and the \"deliver the same event twice,\nassert fulfilled once\" idempotency test are in `references\u002Fwebhooks-and-clocks.md`\n(sections 2–3).\n\n## Failed Payments: Dunning and Refunds\n\nTo test a failed recurring charge end to end, subscribe with `4000000000000341` (SDK token\n`pm_card_chargeCustomerFail`) — it **attaches** to the customer but **fails on the later\ncharge**, which is what the renewal needs. Cards that decline at attach time can't be saved,\nso they can't model a renewal failure.\n\nDrive the lifecycle with a test clock:\n1. Subscribe the customer (on a test clock) with the attach-then-fail card.\n2. `advance` the clock past the renewal date → Stripe attempts the charge.\n3. The charge fails → Stripe emits **`invoice.payment_failed`** and the subscription goes\n   **`past_due`**. Assert both.\n4. Resolve by issuing a refund with **`refunds.create`** (fires `charge.refunded`) — do\n   **not** \"fix\" it by deleting the subscription.\n\nFull driver in `references\u002Fwebhooks-and-clocks.md` (section 5).\n\n## Reconciliation: Fulfill on the Webhook, Not the Redirect\n\nMark an order paid **only** after a signature-verified `payment_intent.succeeded` webhook,\nre-confirmed against the API — never on the `return_url` redirect or a client-side success\nflag, and never by polling with a sleep.\n\n```ts\nif (event.type === 'payment_intent.succeeded') {\n  const verified = await stripe.paymentIntents.retrieve(event.data.object.id);\n  if (verified.status === 'succeeded' && verified.amount_received === expected) {\n    await markOrderPaid(verified.metadata.orderId); \u002F\u002F fulfillment happens HERE\n  }\n}\n```\n\nThe reconciliation test asserts the order is still `pending` after the redirect and only\n`paid` after the verified webhook: `references\u002Fwebhooks-and-clocks.md` (section 6).\n\n## Multi-PSP: Adyen, PayPal, Braintree\n\nStripe test cards do **not** work on other PSPs. Each has its own sandbox cards and sandbox\nbuyer accounts. Port the *structure* of your Stripe tests; swap in the PSP's sandbox values.\nNever reuse Stripe PANs or live\u002Fproduction keys.\n\n- **Adyen** — own test cards (e.g. `4212345678910014` for 3DS2); many declines are driven by\n  the **transaction amount** (`.13` refused, `.51` referral), not the card. Events arrive as\n  HMAC-signed notifications.\n- **PayPal** — log in with a **sandbox buyer account** (sandbox personal email\u002Fpassword), not\n  a card. Confirm server-side via the Orders API \u002F webhooks, not the client `onApprove`.\n- **Braintree** — own sandbox **test card** numbers via Drop-in UI \u002F Hosted Fields; amount\n  drives transaction outcome, card number drives verification.\n\nWhat stays the same: separate test\u002Fsandbox credentials, no real card, and fulfillment on the\nverified server-side event\u002Fnotification. Details: `references\u002Fmulti-psp.md`.\n\n## Anti-Patterns\n\n### 1. Reaching for `4111111111111111`\nThat Luhn-valid number is a Braintree\u002FPayPal-era generic PAN, not a Stripe test card. Use\n`4242424242424242` for success and the specific decline cards (`4000000000000002`,\n`4000000000009995`).\n\n### 2. Treating the card field as a normal input\n`page.locator('#card-number')` silently matches nothing because the field is in a\ncross-origin iframe. Use `frameLocator`.\n\n### 3. Selecting iframes by index\n`page.frames()[1]` breaks the instant Stripe reorders frames. Match the frame by a stable\nname prefix (`iframe[name^=\"__privateStripeFrame\"]`) and chain `frameLocator` for the nested\n3DS challenge.\n\n### 4. `waitForTimeout` to \"wait for the challenge\"\nFlaky on slow CI, wasteful on fast CI. Wait on the element (`expect(...).toBeVisible()` \u002F\nauto-waiting locator actions), never the clock.\n\n### 5. Client-side time mocking for billing\n`jest.useFakeTimers` \u002F sinon \u002F mocking `Date` cannot move Stripe's server-side billing.\nUse a test clock.\n\n### 6. ngrok or polling for local webhooks\n`stripe listen --forward-to localhost:3000\u002Fwebhooks` tunnels events natively; `stripe\ntrigger` fires them. No public tunnel, no status polling.\n\n### 7. Parsing the body before verifying the signature\nA global `express.json()` ahead of the webhook route destroys the raw body\n`constructEvent` needs, so verification can never pass. Mount `express.raw` on the webhook\nroute first.\n\n### 8. Confusing the request idempotency key with webhook dedup, or using an in-memory set\nOutbound idempotency keys don't dedup inbound webhooks; an in-memory `Set` dies on restart.\nPersist `event.id` with a UNIQUE constraint.\n\n### 9. Fulfilling on the redirect \u002F client success flag\nThe `return_url` can be premature, replayed, or forged. Fulfill only on the verified\n`payment_intent.succeeded` webhook.\n\n### 10. \"Fixing\" a failed renewal by deleting the subscription\nThe correct resolution is a refund via `refunds.create`, leaving the dunning lifecycle\n(`invoice.payment_failed` → `past_due`) intact and testable.\n\n### 11. Rationalizing a real card \"just in CI\"\nA hardcoded real PAN is a PCI\u002Fcompliance violation regardless of environment. The fix is a\ntest card in test mode — plus removing the secret from the repo and git history and rotating\nany exposed key. Masking or encrypting it does not make it acceptable.\n\n## Verification\n\n- `stripe listen --forward-to localhost:3000\u002Fwebhooks` prints a `whsec_…` and shows events\n  arriving when you run `stripe trigger payment_intent.succeeded`.\n- Running the 3DS test with `4000000000003220` reaches and clicks the **Complete\n  authentication** button (the test fails loudly, not silently, if the nested frame isn't\n  found).\n- The signature test: a tampered `Stripe-Signature` returns **400**; a header from\n  `generateTestHeaderString` returns **200**.\n- The idempotency test: delivering the same `event.id` twice fulfills **once**.\n- `grep -rE 'pk_live|sk_live|4111111111111111'` over the test suite returns nothing.\n\n## Done When\n\n- Checkout suite covers success (`4242424242424242`), `card_declined`\n  (`4000000000000002`), and `insufficient_funds` (`4000000000009995`), each asserting the\n  matching outcome, using `pk_test_`\u002F`sk_test_` keys.\n- A 3DS test fills `4000000000003220`, reaches the nested challenge frame via chained\n  `frameLocator`, clicks **Complete authentication**, and asserts the succeeded state — no\n  `frames()[index]`, no `waitForTimeout`.\n- A subscription-renewal test uses a Stripe test clock (`testHelpers.testClocks.create` +\n  `advance`, forward-only, customer attached at creation) instead of any client-side time mock.\n- Local webhooks are received via `stripe listen --forward-to` \u002F `stripe trigger`, with the\n  `whsec_…` wired into `STRIPE_WEBHOOK_SECRET` (distinct from `STRIPE_SECRET_KEY`).\n- The webhook handler verifies the signature with `constructEvent` on the **raw** body\n  before parsing, returns 400 on a forged event, and a test proves it.\n- Idempotency is enforced by persisting `event.id` with a UNIQUE constraint; a duplicate\n  delivery returns 200 and fulfills exactly once, proven by a test.\n- A failed-renewal test drives `invoice.payment_failed` → `past_due` → `refunds.create` via\n  a test clock and the attach-then-fail card `4000000000000341`.\n- A reconciliation test confirms the order is `paid` only after the verified\n  `payment_intent.succeeded` webhook (re-checked with `paymentIntents.retrieve`), not on the\n  redirect.\n- `grep -rE 'pk_live|sk_live|4111111111111111'` finds no live key or banned PAN in the test\n  suite. (A bare `[0-9]{16}` scan would false-positive on every legitimate test card —\n  match live-key prefixes and the banned `4111…` number, not all 16-digit strings.)\n\n## Related Skills\n\n- **api-testing** — General REST\u002FGraphQL endpoint testing, schema validation, and auth\n  flows for non-payment endpoints. Go there when the target isn't a PSP checkout\u002Fwebhook.\n- **playwright-automation** — Page Object Model, fixtures, and general browser E2E\n  mechanics that the 3DS flow here builds on.\n- **compliance-testing** — PCI-DSS, GDPR, and regulatory audit work. This skill keeps you\n  *out* of PCI scope by using test cards; go there for a formal compliance audit.\n- **test-data-management** — Seeding customers, subscriptions, and fixtures; managing the\n  test-clock-bound customers this skill creates.\n- **qa-project-context** — The universal first stop: PSP, stack, and fixture conventions\n  that every question above should defer to.\n\n## Reference Files (in `references\u002F`)\n\n- **stripe-test-cards.md** — Full test-card catalogue with decline codes and the Playwright\n  success\u002Fdecline\u002Finsufficient-funds tests.\n- **playwright-3ds.md** — Nested-iframe 3DS challenge handling, complete and fail variants,\n  and selector notes.\n- **webhooks-and-clocks.md** — `stripe listen`\u002F`trigger`, raw-body signature verification,\n  idempotency by `event.id`, test clocks, failed-renewal dunning + refunds, and\n  reconciliation.\n- **multi-psp.md** — Adyen, PayPal, and Braintree sandbox patterns and what differs from\n  Stripe.\n",{"name":5,"license":15,"metadata":54,"description":7},{"author":55,"version":56,"category":43},"kindlmann","1.0","claude\u002Finstall-gsd-build-foQdV","d86cd127f527124b5fd0fd0bc679a4d79b7d01d7",".agents\u002Fskills\u002Fpayment-testing","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik","https:\u002F\u002Fnorvik-orpin.vercel.app","https:\u002F\u002Fgithub.com\u002Fpattanation","2026-07-17 11:17:04",{"source":8,"skillName":5,"commands":65,"prompt":75,"zipAvailable":76},[66,69,72],{"tool":67,"command":68},"npx","npx skills add pattanation\u002FNorvik --skill payment-testing",{"tool":70,"command":71},"bunx","bunx skills add pattanation\u002FNorvik --skill payment-testing",{"tool":73,"command":74},"pnpm","pnpm dlx skills add pattanation\u002FNorvik --skill payment-testing","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fpayment-testing\u002FSKILL.md",true,[78,84,88,92],{"path":79,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":82,"sourceHtmlUrl":83},"references\u002Fmulti-psp.md","REFERENCE","参考文档",2601,"https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fpayment-testing\u002Freferences\u002Fmulti-psp.md",{"path":85,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":86,"sourceHtmlUrl":87},"references\u002Fplaywright-3ds.md",3908,"https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fpayment-testing\u002Freferences\u002Fplaywright-3ds.md",{"path":89,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":90,"sourceHtmlUrl":91},"references\u002Fstripe-test-cards.md",3700,"https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fpayment-testing\u002Freferences\u002Fstripe-test-cards.md",{"path":93,"resourceType":80,"resourceTypeDesc":81,"sizeBytes":94,"sourceHtmlUrl":95},"references\u002Fwebhooks-and-clocks.md",9805,"https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fpayment-testing\u002Freferences\u002Fwebhooks-and-clocks.md",[97,126,151,173,195,210],{"skillNo":98,"name":99,"nameZh":6,"description":100,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":101,"licenseFileUrl":102,"sourceHtmlUrl":103,"categories":104,"keywords":109,"publishedAt":125},"01M71QDV4DM8T19HEHWATEF1K4","webapp-testing","Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.","Apache-2.0","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fwebapp-testing\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fwebapp-testing\u002FSKILL.md",[105,106],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":107,"categoryName":108,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",[110,31,111,37,112,113,114,115,116,117,118,119,120,121,122,123,124],"webapp","webapp testing","applications","local","frontend","debugging","toolkit","logs","browser","with","playwright","web","behavior","ui","screenshots","2026-11-10 13:02:02",{"skillNo":127,"name":128,"nameZh":6,"description":129,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":101,"licenseFileUrl":130,"sourceHtmlUrl":131,"categories":132,"keywords":134,"publishedAt":125},"01M71QDH2GGMXGA9105C9W1N7B","web-artifacts-builder","Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn\u002Fui). Use for complex artifacts requiring state management, routing, or shadcn\u002Fui components - not for simple single-file HTML\u002FJSX artifacts.","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fweb-artifacts-builder\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fweb-artifacts-builder\u002FSKILL.md",[133],{"categoryNo":107,"categoryName":108,"taxonomyType":21,"taxonomyTypeDesc":22},[135,136,137,121,138,139,140,37,141,142,114,143,33,144,145,146,147,148,149,150],"builder","artifacts builder","artifacts","web artifacts builder","single-file","css","Tailwind","ui components","single","file","components","react","multi","state","routing","state management",{"skillNo":152,"name":153,"nameZh":6,"description":154,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":155,"sourceHtmlUrl":156,"categories":157,"keywords":159,"publishedAt":125},"01M71QD9T2XC3EWMQNN9ACNWQ1","vueuse-functions","Apply VueUse composables where appropriate to build concise, maintainable Vue.js \u002F Nuxt features.","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fvueuse-functions\u002FLICENSE.md","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fvueuse-functions\u002FSKILL.md",[158],{"categoryNo":107,"categoryName":108,"taxonomyType":21,"taxonomyTypeDesc":22},[160,161,162,47,163,164,165,166,167,168,169,170,171,172],"functions","vueuse","vueuse functions","nuxt","apply","github","concise","build","https","features","vue","js","composables",{"skillNo":174,"name":175,"nameZh":6,"description":176,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":177,"sourceHtmlUrl":178,"categories":179,"keywords":182,"publishedAt":125},"01M71QD51HWSGYMCMB7RW7PGK8","vue-testing-best-practices","Use for Vue.js testing. Covers Vitest, Vue Test Utils, component testing, mocking, testing patterns, and Playwright for E2E testing.","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fvue-testing-best-practices\u002FLICENSE.md","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fvue-testing-best-practices\u002FSKILL.md",[180,181],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":107,"categoryName":108,"taxonomyType":21,"taxonomyTypeDesc":22},[183,31,184,170,185,120,186,187,188,165,189,37,190,191,192,33,193,171,194],"vue testing best practices","testing best practices","best practices","test","testing patterns","component","vitest","mocking","e2e","utils","patterns","e2e testing",{"skillNo":196,"name":197,"nameZh":6,"description":198,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":199,"sourceHtmlUrl":200,"categories":201,"keywords":203,"publishedAt":125},"01M71QD358H2DJ9THJRBP77T3T","vue-router-best-practices","Vue Router 4 patterns, navigation guards, route params, and route-component lifecycle interactions.","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fvue-router-best-practices\u002FLICENSE.md","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fvue-router-best-practices\u002FSKILL.md",[202],{"categoryNo":107,"categoryName":108,"taxonomyType":21,"taxonomyTypeDesc":22},[204,170,205,185,206,188,207,165,193,208,209],"router","vue router best practices","route","guards","navigation","lifecycle",{"skillNo":211,"name":212,"nameZh":6,"description":213,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":214,"sourceHtmlUrl":215,"categories":216,"keywords":218,"publishedAt":125},"01M71QCYPNWDE2MQZ1TF3ED5S4","vue-best-practices","MUST be used for Vue.js tasks. Strongly recommends Composition API with `\u003Cscript setup>` and TypeScript as the standard approach. Covers Vue 3, SSR, Volar, vue-tsc. Load for any Vue, .vue files, Vue Router, Pinia, or Vite with Vue work. ALWAYS use Composition API unless the project explicitly requires Options API.","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fvue-best-practices\u002FLICENSE.md","https:\u002F\u002Fgithub.com\u002Fpattanation\u002FNorvik\u002Fblob\u002Fclaude\u002Finstall-gsd-build-foQdV\u002F.agents\u002Fskills\u002Fvue-best-practices\u002FSKILL.md",[217],{"categoryNo":107,"categoryName":108,"taxonomyType":21,"taxonomyTypeDesc":22},[219,170,185,220,47,221,222,223,204,224,225,226,227,228,229,37,230,33,231,232],"vue best practices","Composition","files","tasks","api","pinia","work","options","ssr","typescript","as","vite","load","standard"]