@variance-authority/core 0.14.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/CHANGELOG.md +878 -0
  2. package/README.md +38 -17
  3. package/dist/attribute/component-hash.d.ts +14 -5
  4. package/dist/attribute/component-hash.js +14 -5
  5. package/dist/attribute/component-hash.js.map +1 -1
  6. package/dist/attribute/composition.d.ts +16 -0
  7. package/dist/attribute/composition.js +4 -6
  8. package/dist/attribute/composition.js.map +1 -1
  9. package/dist/attribute/index.d.ts +3 -1
  10. package/dist/attribute/index.js +2 -1
  11. package/dist/attribute/index.js.map +1 -1
  12. package/dist/attribute/movement.d.ts +1 -1
  13. package/dist/attribute/movement.js.map +1 -1
  14. package/dist/attribute/region.d.ts +47 -4
  15. package/dist/attribute/region.js +91 -22
  16. package/dist/attribute/region.js.map +1 -1
  17. package/dist/attribute/source.d.ts +10 -24
  18. package/dist/attribute/source.js +4 -38
  19. package/dist/attribute/source.js.map +1 -1
  20. package/dist/attribute/subject-pieces.d.ts +37 -0
  21. package/dist/attribute/subject-pieces.js +122 -0
  22. package/dist/attribute/subject-pieces.js.map +1 -0
  23. package/dist/compare/band.d.ts +8 -2
  24. package/dist/compare/band.js +7 -2
  25. package/dist/compare/band.js.map +1 -1
  26. package/dist/compare/diff/attribution.js +6 -4
  27. package/dist/compare/diff/attribution.js.map +1 -1
  28. package/dist/compare/diff/compare-nodes.js +9 -1
  29. package/dist/compare/diff/compare-nodes.js.map +1 -1
  30. package/dist/compare/diff/match.d.ts +2 -1
  31. package/dist/compare/diff/match.js +52 -0
  32. package/dist/compare/diff/match.js.map +1 -1
  33. package/dist/compare/parting.js +6 -4
  34. package/dist/compare/parting.js.map +1 -1
  35. package/dist/format/capture.d.ts +25 -0
  36. package/dist/format/capture.js +6 -0
  37. package/dist/format/capture.js.map +1 -1
  38. package/dist/format/document.d.ts +28 -5
  39. package/dist/format/document.js +8 -2
  40. package/dist/format/document.js.map +1 -1
  41. package/dist/format/drive.d.ts +191 -0
  42. package/dist/format/drive.js +343 -0
  43. package/dist/format/drive.js.map +1 -0
  44. package/dist/format/environment.d.ts +20 -2
  45. package/dist/format/environment.js +7 -0
  46. package/dist/format/environment.js.map +1 -1
  47. package/dist/format/index.d.ts +13 -3
  48. package/dist/format/index.js +8 -2
  49. package/dist/format/index.js.map +1 -1
  50. package/dist/format/intervention.d.ts +79 -0
  51. package/dist/format/intervention.js.map +1 -1
  52. package/dist/format/origin.d.ts +32 -0
  53. package/dist/format/origin.js +54 -0
  54. package/dist/format/origin.js.map +1 -0
  55. package/dist/format/packed.d.ts +32 -0
  56. package/dist/format/packed.js +23 -0
  57. package/dist/format/packed.js.map +1 -0
  58. package/dist/format/shape.d.ts +31 -0
  59. package/dist/format/shape.js +269 -0
  60. package/dist/format/shape.js.map +1 -0
  61. package/dist/format/snapshot.d.ts +2 -0
  62. package/dist/format/snapshot.js.map +1 -1
  63. package/dist/format/stabilize.d.ts +35 -6
  64. package/dist/format/stabilize.js +58 -103
  65. package/dist/format/stabilize.js.map +1 -1
  66. package/dist/format/steps.d.ts +44 -0
  67. package/dist/format/steps.js +67 -0
  68. package/dist/format/steps.js.map +1 -0
  69. package/dist/format/waits.d.ts +12 -0
  70. package/dist/format/waits.js +156 -0
  71. package/dist/format/waits.js.map +1 -0
  72. package/dist/format/width.d.ts +35 -0
  73. package/dist/format/width.js +38 -0
  74. package/dist/format/width.js.map +1 -0
  75. package/dist/index.d.ts +2 -1
  76. package/dist/index.js +2 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/judge/docket.js +1 -0
  79. package/dist/judge/docket.js.map +1 -1
  80. package/dist/judge/index.d.ts +1 -0
  81. package/dist/judge/index.js +1 -0
  82. package/dist/judge/index.js.map +1 -1
  83. package/dist/judge/restamp.d.ts +20 -0
  84. package/dist/judge/restamp.js +20 -0
  85. package/dist/judge/restamp.js.map +1 -0
  86. package/dist/nearest.d.ts +46 -0
  87. package/dist/nearest.js +102 -0
  88. package/dist/nearest.js.map +1 -0
  89. package/dist/relate/before.d.ts +19 -24
  90. package/dist/relate/before.js +37 -39
  91. package/dist/relate/before.js.map +1 -1
  92. package/dist/relate/beyond.d.ts +59 -0
  93. package/dist/relate/beyond.js +98 -0
  94. package/dist/relate/beyond.js.map +1 -0
  95. package/dist/relate/carried.d.ts +122 -0
  96. package/dist/relate/carried.js +279 -0
  97. package/dist/relate/carried.js.map +1 -0
  98. package/dist/relate/graph.d.ts +14 -12
  99. package/dist/relate/graph.js +12 -11
  100. package/dist/relate/graph.js.map +1 -1
  101. package/dist/relate/index.d.ts +3 -1
  102. package/dist/relate/index.js +3 -1
  103. package/dist/relate/index.js.map +1 -1
  104. package/dist/relate/merkle.js +2 -11
  105. package/dist/relate/merkle.js.map +1 -1
  106. package/dist/relate/narrow.d.ts +2 -2
  107. package/dist/relate/narrow.js +26 -73
  108. package/dist/relate/narrow.js.map +1 -1
  109. package/dist/relate/reach.d.ts +6 -6
  110. package/dist/relate/reach.js +10 -53
  111. package/dist/relate/reach.js.map +1 -1
  112. package/dist/relate/records.d.ts +6 -5
  113. package/dist/relate/records.js +3 -2
  114. package/dist/relate/records.js.map +1 -1
  115. package/dist/rules/index.d.ts +3 -3
  116. package/dist/rules/index.js +3 -3
  117. package/dist/rules/index.js.map +1 -1
  118. package/dist/rules/normalize/cascade.d.ts +12 -0
  119. package/dist/rules/normalize/cascade.js +34 -16
  120. package/dist/rules/normalize/cascade.js.map +1 -1
  121. package/dist/rules/normalize/fixture.d.ts +3 -0
  122. package/dist/rules/normalize/fixture.js +3 -1
  123. package/dist/rules/normalize/fixture.js.map +1 -1
  124. package/dist/rules/normalize/index.d.ts +2 -2
  125. package/dist/rules/normalize/index.js +9 -2
  126. package/dist/rules/normalize/index.js.map +1 -1
  127. package/dist/rules/normalize/value.d.ts +13 -2
  128. package/dist/rules/normalize/value.js +42 -8
  129. package/dist/rules/normalize/value.js.map +1 -1
  130. package/dist/rules/normalize/wrapper.js +5 -0
  131. package/dist/rules/normalize/wrapper.js.map +1 -1
  132. package/dist/rules/ruleset.d.ts +24 -4
  133. package/dist/rules/ruleset.js +24 -4
  134. package/dist/rules/ruleset.js.map +1 -1
  135. package/dist/segment/index.d.ts +20 -4
  136. package/dist/segment/index.js +28 -7
  137. package/dist/segment/index.js.map +1 -1
  138. package/dist/shard/index.d.ts +11 -0
  139. package/dist/shard/index.js +11 -0
  140. package/dist/shard/index.js.map +1 -0
  141. package/dist/shard/placement.d.ts +119 -0
  142. package/dist/shard/placement.js +156 -0
  143. package/dist/shard/placement.js.map +1 -0
  144. package/dist/share/index.d.ts +1 -1
  145. package/dist/share/index.js +1 -1
  146. package/dist/share/index.js.map +1 -1
  147. package/package.json +8 -1
@@ -1 +1 @@
1
- {"version":3,"file":"document.js","sourceRoot":"","sources":["../../src/format/document.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAe,MAAM,WAAW,CAAC;AA8JrD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,cAAc,CAAC,QAAwB;IACrD,OAAO,WAAW,CAAC;QACjB,eAAe,EAAE,QAAQ,CAAC,eAAe;QACzC,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,KAAK,EAAE;YACL,IAAI,EAAE,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE;YAChC,IAAI,EAAE,EAAE,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE;YAChC,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBACpD,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,UAAU,EAAE,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE;aACtC,CAAC,CAAC;YACH,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,KAAK,SAAS;gBAC7C,CAAC,CAAC,EAAE,cAAc,EAAE,QAAQ,CAAC,KAAK,CAAC,cAAc,EAAE;gBACnD,CAAC,CAAC,EAAE,CAAC;SACR;QACD,GAAG,EAAE,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC;QACtB,QAAQ,EAAE,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE;QAClC,SAAS,EAAE,EAAE,GAAG,QAAQ,CAAC,SAAS,EAAE;QACpC,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC;QAC1B,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,GAAG,CAAC,QAAQ,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxE,GAAG,CAAC,QAAQ,CAAC,SAAS,KAAK,SAAS;YAClC,CAAC,CAAC;gBACE,SAAS,EAAE,MAAM,CAAC,WAAW,CAC3B,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC;oBAC1D,GAAG;oBACH,EAAE,GAAG,QAAQ,EAAE;iBAChB,CAAC,CACH;aACF;YACH,CAAC,CAAC,EAAE,CAAC;KACR,CAAC,CAAC;AACL,CAAC;AAyDD,MAAM,UAAU,cAAc,CAAC,QAAwB;IACrD,OAAO,WAAW,CAAC;QACjB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,GAAG,CAAC,QAAQ,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1F,GAAG,CAAC,QAAQ,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,QAAQ,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1F,iBAAiB,EAAE,QAAQ,CAAC,iBAAiB;QAC7C,KAAK,EAAE,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC;KAC3B,CAAC,CAAC;AACL,CAAC;AAuGD,iFAAiF;AACjF,MAAM,UAAU,QAAQ,CAAC,MAAc;IACrC,OAAO,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC;AACjG,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,MAA6B;IAC1D,OAAO,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC;AACnE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CAAC,MAAc,EAAE,OAAe;IACrD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,KAAK,OAAO,uDAAuD;YACjE,yDAAyD,CAC5D,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import type { Diagnostic, SubjectRef } from './capture.js';\nimport type { Viewport } from './environment.js';\nimport { digestValue, type Digest } from './hash.js';\nimport type { ComponentHash } from './snapshot.js';\n\n/**\n * The render document: what gets *sent* somewhere to become an image.\n *\n * This is the acquisition phase's output and the only thing the rendering side\n * ever sees. It is plain serializable data for the same reason `RawCapture` is\n * (ADR-0006): the renderer may be a page in this process, a browser on this\n * machine, or a pinned container two networks away, and no code upstream of it\n * may assume which.\n *\n * Two routes produce one of these, and the point of the type is that they are\n * interchangeable:\n *\n * 1. **jsdom → document → render elsewhere.** jsdom cannot rasterize. It can\n * describe exactly what to rasterize, which is the sub-renderer split ADR-0002\n * asks for: the cheap tier decides almost everything, and the residue is\n * handed to something that owns a GPU.\n * 2. **playwright → document → render on a server.** The same payload, acquired\n * from a real engine. Used when the *deciding* machine and the *pinned*\n * machine are different, which is the case every CI setup actually has.\n *\n * A browser may instead capture a `Raster` in place. That is the sibling branch\n * of `CaptureMaterial`, not a third route that somehow produces this type.\n *\n * ## Why this is small\n *\n * `css` holds the *applicable* rules only — the pruning of ADR-0003, which on\n * `@variance-authority/dom`'s accretion fixture takes 1010 rules to 1. That\n * number is a cost claim as much as a\n * correctness one: shipping a document over a network hop is only sensible if the\n * document is not the entire design system plus Storybook's chrome.\n */\nexport interface RenderDocument {\n readonly documentVersion: 1;\n readonly subject: SubjectRef;\n\n /**\n * The subject subtree, serialized.\n *\n * Serialized rather than referenced by URL: a URL makes the renderer\n * responsible for reproducing the application's state, which it cannot do and\n * should not try. What is sent is the markup that existed at the moment of\n * capture, which is the only definition of \"this render\" that survives a hop.\n */\n readonly html: string;\n\n /**\n * The ancestor context the subject was rendered inside.\n *\n * Not decoration. Pruning keeps rules like `html.dark .card` and\n * `.app .list > li`, whose left-hand side lives *above* the subject — render\n * the subtree bare and those rules match nothing, so the image is missing\n * exactly the styling the collector went to the trouble of proving applies.\n *\n * Reproduced as empty open tags rather than by re-rendering the application:\n * what a selector needs is a chain of elements with the right tags, ids,\n * classes, and attributes, and nothing above the subject contributes anything\n * else to it that {@link RenderDocument.inherited} does not already carry.\n */\n readonly frame: RenderFrame;\n\n /**\n * Applicable stylesheet text, in cascade order.\n *\n * Order is load-bearing and is the caller's to preserve — the cascade breaks\n * specificity ties by document order, so a set that arrives shuffled paints\n * differently while hashing the same if this were a set rather than a list.\n */\n readonly css: readonly string[];\n\n readonly viewport: Viewport;\n\n /**\n * Inherited values in force at the subject root, applied to its wrapper.\n *\n * The same requirement as `RawCapture.inheritedSeed` and for the same reason:\n * pruning drops rules on ancestors outside the subtree, and a font-size\n * inherited from `<html>` is not optional decoration — dropping it changes\n * every metric in the image.\n */\n readonly inherited: Readonly<Record<string, string>>;\n\n /**\n * Fonts the acquiring side observed as loaded.\n *\n * Carried, not resolved. A renderer that lacks one of these will substitute\n * and produce different metrics, and that has to be *detectable* rather than\n * silently absorbed — see {@link RenderIdentity}.\n */\n readonly fonts: readonly string[];\n\n /** External references, url → content hash. Present so a swap is visible. */\n readonly assets?: Readonly<Record<string, string>>;\n\n /**\n * Base against which relative resource references were resolved.\n *\n * Preserves relative-reference resolution for either document kind. Resource\n * closure is denoted by `resources`, not by the presence of this field.\n */\n readonly baseUrl?: string;\n\n /**\n * Immutable responses needed to paint this document, keyed by absolute URL.\n *\n * Presence — including an empty object — means the document is\n * resource-closed: a renderer must not consult the network for a missing URL.\n * Absence preserves the older network-capable document contract and must not\n * be described as portable to another machine.\n */\n readonly resources?: Readonly<Record<string, RenderResource>>;\n\n readonly diagnostics: readonly Diagnostic[];\n}\n\nexport interface RenderResource {\n readonly contentType: string;\n /** Response bytes, base64. */\n readonly bytes: string;\n readonly digest: Digest;\n\n /**\n * The status a renderer answers this URL with. Absent means 200.\n *\n * A subject is allowed to reference something that is not there: a fallback\n * test does it on purpose, and the browser it was acquired in painted the\n * broken state rather than the image. Recording the failure as a failure is\n * what lets a renderer reproduce that state without asking the network.\n */\n readonly status?: number;\n}\n\nexport interface RenderFrame {\n /** Attributes on `<html>`. `class=\"dark\"` here decides half a design system. */\n readonly html: Readonly<Record<string, string>>;\n readonly body: Readonly<Record<string, string>>;\n /** Elements between `<body>` and the subject root, outermost first. */\n readonly ancestors: readonly FrameElement[];\n\n /**\n * Used width of the subject's parent box, in CSS pixels, when layout was\n * observable.\n *\n * A subject declaring `width: 100%` is a different size in a 1024px page and\n * inside a 600px panel, and nothing else in this document says which it was.\n * Absent under a profile with no layout engine, in which case the render falls\n * back to the viewport and the difference, if any, is real rather than hidden.\n */\n readonly containerWidth?: number;\n}\n\nexport interface FrameElement {\n readonly tag: string;\n readonly attributes: Readonly<Record<string, string>>;\n}\n\n/**\n * Content address of a document: *what is to be painted*.\n *\n * This is the deferral lever. Rendering is the expensive phase, and a document\n * whose digest has been rendered before under the same identity has an image\n * already — so the cheapest render is the one that is skipped. Nothing about the\n * branch, the commit, or the file path enters this (Principle 4), so moving a\n * story between files does not cost a re-render.\n *\n * Deliberately covers `viewport` in full, `deviceScaleFactor` included: unlike\n * the semantic key (ADR-0010), a raster genuinely differs at 2x and the whole\n * purpose of this digest is to address the raster.\n *\n * **Limit, stated rather than discovered.** Declaration values arrive serialized\n * by the acquiring engine's CSSOM — jsdom writes `rgb(18, 52, 86)` where the\n * author wrote `#123456` — and this phase deliberately does not canonicalize\n * them, because a collector that normalizes is a second ruleset versioned by\n * nothing (ADR-0001). So two engines can address the same page differently. That\n * makes a render cache *per acquiring engine*, which is a missed cache hit and\n * never a wrong image.\n */\nexport function documentDigest(document: RenderDocument): Digest {\n return digestValue({\n documentVersion: document.documentVersion,\n html: document.html,\n frame: {\n html: { ...document.frame.html },\n body: { ...document.frame.body },\n ancestors: document.frame.ancestors.map((element) => ({\n tag: element.tag,\n attributes: { ...element.attributes },\n })),\n ...(document.frame.containerWidth !== undefined\n ? { containerWidth: document.frame.containerWidth }\n : {}),\n },\n css: [...document.css],\n viewport: { ...document.viewport },\n inherited: { ...document.inherited },\n fonts: [...document.fonts],\n ...(document.assets ? { assets: { ...document.assets } } : {}),\n ...(document.baseUrl !== undefined ? { baseUrl: document.baseUrl } : {}),\n ...(document.resources !== undefined\n ? {\n resources: Object.fromEntries(\n Object.entries(document.resources).map(([url, resource]) => [\n url,\n { ...resource },\n ]),\n ),\n }\n : {}),\n });\n}\n\n/**\n * Who painted it.\n *\n * The durable half of this system rests on one requirement — *images compared\n * against each other must come from the same machine* — and a requirement that\n * is merely stated gets violated by a CI runner upgrade nobody announced. This\n * makes it a value, so it can be compared rather than assumed.\n *\n * Every field is something that has been observed to move pixels without moving\n * markup. None of them is a guess about what *might* matter.\n */\nexport interface RenderIdentity {\n /** `playwright-chromium@1.49.0`, `remote:render.internal`, … */\n readonly renderer: string;\n /** Engine build. A Chromium bump repaints text; that is not a regression. */\n readonly engine: string;\n /** OS and architecture. Font rasterization differs across both. */\n readonly platform: string;\n readonly deviceScaleFactor: number;\n /**\n * Fonts the *renderer* actually has.\n *\n * Compared against {@link RenderDocument.fonts} at render time, because a\n * substituted font is the single most common way two machines disagree, and\n * it is invisible in every artifact except the image itself.\n */\n readonly fonts: readonly string[];\n\n /**\n * Digest of what the renderer did *to* the page so it could be observed.\n *\n * Pausing animations, hiding a caret, waiting for fonts — each is necessary\n * and each widens the gap between what was measured and what a person sees.\n * The gap itself is acceptable; a *difference* in it between two runs is not,\n * because the images then differ for a reason that is not the code and the\n * report blames whichever component sits under the pixels. Folding it in here\n * makes that comparison `incomparable` instead, by the same mechanism that\n * refuses a cross-machine one.\n *\n * Optional only because a renderer may predate the field. Absent means the\n * renderer did not record what it did, which is emphatically not the same as\n * \"it did nothing\" — and two such renderers will compare, which is the hazard\n * that argues for every renderer setting it.\n */\n readonly stabilization?: Digest;\n\n /**\n * Digest of pixel-affecting browser launch and raster settings.\n *\n * Separate from stabilization: one describes how the browser paints, the\n * other what was done to the page before it was read.\n */\n readonly rasterization?: Digest;\n}\n\nexport function identityDigest(identity: RenderIdentity): Digest {\n return digestValue({\n renderer: identity.renderer,\n engine: identity.engine,\n platform: identity.platform,\n ...(identity.stabilization !== undefined ? { stabilization: identity.stabilization } : {}),\n ...(identity.rasterization !== undefined ? { rasterization: identity.rasterization } : {}),\n deviceScaleFactor: identity.deviceScaleFactor,\n fonts: [...identity.fonts],\n });\n}\n\n/**\n * A rendered image and the conditions it was produced under.\n *\n * `bytes` is base64 rather than a `Buffer` so the whole value survives the same\n * hops the document does. A raster that could only exist in Node would make the\n * remote route a special case in every function that touches one.\n *\n * The image is optional, and the three fields that describe it move together.\n * A subject that occupies no pixels — a wrapper whose only child went to a\n * portal, a `describeConformance` mount with no children — still has a\n * document, rules, components and an accessibility tree, all of which compare.\n * Refusing to photograph it is right; refusing the subject reported a quarter\n * of Material UI's unit tier as unobserved over an axis nothing was asking\n * about. Absent here means *this subject has no pixels to observe*, which is a\n * measurement; it never means *the image was lost*.\n */\nexport interface Raster {\n readonly documentDigest: Digest;\n readonly identity: RenderIdentity;\n /**\n * Device pixels. `width / viewport.width` is the scale, and is asserted.\n *\n * Absent exactly when {@link bytes} is, and {@link pictured} is the one place\n * that reads the three together.\n */\n readonly width?: number;\n readonly height?: number;\n /** PNG, base64. Absent when the subject occupies no pixels. */\n readonly bytes?: string;\n /** Fonts the document declared that the renderer did not have. */\n readonly missingFonts: readonly string[];\n\n /**\n * The browser-computed accessibility tree from the acquisition that produced\n * this candidate. It is independent evidence: pixel equality cannot settle it.\n * Absent means the acquisition host did not observe this boundary.\n */\n readonly accessibility?: import('./accessibility.js').AccessibilitySnapshot;\n\n /**\n * What the document that painted this said about its own components (ADR-0018).\n *\n * The field that makes a baseline self-describing, and the reason it is here\n * rather than only in a history store. Separating the component that *caused* a\n * change from the components the change merely moved needs both revisions, and\n * a stored baseline is an image: without this, a run has one document and ranks\n * by area — which\n * [journal 0013](../../../../docs/context/journal/0013-observability.md)\n * measured as backwards.\n *\n * Text, and small: one line per component boundary. It rides in the sidecar\n * beside a PNG that dominates it, so a baseline carries its own explanation\n * wherever it is copied, and a store or a transport that drops it degrades to\n * ranking by area rather than to a wrong answer.\n *\n * Optional because a baseline written before this existed does not have it, and\n * because a profile or a collector that supplies no snapshot cannot produce it.\n * Absent means *unknown*, never *no components*.\n */\n readonly components?: readonly ComponentHash[];\n\n /**\n * What inspection found in the document that painted this, as marks.\n *\n * The field that lets a later run say *this defect was already here* instead of\n * printing every defect it finds with no indication of when it arrived. A\n * finding is produced from one render with no baseline consulted — that is the\n * point of having one — and the consequence is that the list is identical on\n * the run that introduced a defect and on the two hundred runs after it. A\n * reviewer reading `a control inside another control` has no way to tell an\n * inherited condition from something they just wrote, and the two are different\n * decisions.\n *\n * Marks rather than findings, from {@link findingMark}. The sentence, the\n * landmark phrase and the source line are all rewritten by edits that do not\n * touch the defect, so storing them would report a defect as new every time the\n * copy beside it changed — and they are the bulk of the bytes, in a sidecar\n * that is committed beside every baseline.\n *\n * **Absent means nothing inspected it, never that it was clean.** A baseline\n * written before this existed, or by a collector that supplies no snapshot, has\n * no list, and a run that read absence as `[]` would announce every standing\n * defect in the suite as newly introduced on the first run after an upgrade.\n */\n readonly findingMarks?: readonly string[];\n}\n\n/**\n * A raster whose image is in hand.\n *\n * Every pixel comparison in the codebase needs all three of `bytes`, `width`\n * and `height`, and they are absent together or present together. Narrowing\n * them one at a time is how a caller ends up reading the bytes of a subject\n * that was never photographed, so the narrowing lives here and is done once.\n */\nexport type Pictured = Raster & {\n readonly width: number;\n readonly height: number;\n readonly bytes: string;\n};\n\n/** Whether this record carries an image, narrowing all three fields together. */\nexport function pictured(raster: Raster): raster is Pictured {\n return raster.bytes !== undefined && raster.width !== undefined && raster.height !== undefined;\n}\n\n/**\n * Whether the subject occupied any pixels, answered without the image.\n *\n * The question {@link pictured} cannot be asked of a sidecar. Bytes live in a\n * separate file, row or object, so a record read without them carries the width\n * and the height and nothing else — and those *are* the store's record of\n * whether there was ever anything to photograph, which is why `sidecarFrom`\n * refuses a sidecar carrying one of them.\n *\n * So: this for a record, {@link pictured} for an image in hand. A caller that\n * reached for `pictured` here would read every sidecar in the store as a subject\n * with no pixels, because none of them have bytes.\n */\nexport function occupiesPixels(raster: Omit<Raster, 'bytes'>): boolean {\n return raster.width !== undefined && raster.height !== undefined;\n}\n\n/**\n * The image, or a refusal naming the subject that has none.\n *\n * For the paths where a missing image is a programming error rather than a\n * measurement — a diff, a PNG encode — so they read as assertions instead of\n * as a non-null assertion nobody can audit.\n */\nexport function picture(raster: Raster, subject: string): Pictured {\n if (!pictured(raster)) {\n throw new Error(\n `\\`${subject}\\` occupies no pixels, and this path needs an image; ` +\n 'a subject with no raster compares on its document alone',\n );\n }\n return raster;\n}\n"]}
1
+ {"version":3,"file":"document.js","sourceRoot":"","sources":["../../src/format/document.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Diagnostic, SubjectRef } from './capture.js';\nimport type { Viewport } from './environment.js';\nimport { digestValue, type Digest } from './hash.js';\nimport { pageOrigin, withoutOrigin } from './origin.js';\nimport type { PackedSnapshot } from './packed.js';\nimport type { ComponentHash } from './snapshot.js';\n\n/**\n * The render document: what gets *sent* somewhere to become an image.\n *\n * This is the acquisition phase's output and the only thing the rendering side\n * ever sees. It is plain serializable data for the same reason `RawCapture` is\n * (ADR-0006): the renderer may be a page in this process, a browser on this\n * machine, or a pinned container two networks away, and no code upstream of it\n * may assume which.\n *\n * Two routes produce one of these, and the point of the type is that they are\n * interchangeable:\n *\n * 1. **jsdom → document → render elsewhere.** jsdom cannot rasterize. It can\n * describe exactly what to rasterize, which is the sub-renderer split ADR-0002\n * asks for: the cheap tier decides almost everything, and the residue is\n * handed to something that owns a GPU.\n * 2. **playwright → document → render on a server.** The same payload, acquired\n * from a real engine. Used when the *deciding* machine and the *pinned*\n * machine are different, which is the case every CI setup actually has.\n *\n * A browser may instead capture a `Raster` in place. That is the sibling branch\n * of `CaptureMaterial`, not a third route that somehow produces this type.\n *\n * ## Why this is small\n *\n * `css` holds the *applicable* rules only — the pruning of ADR-0003, which on\n * `@variance-authority/dom`'s accretion fixture takes 1010 rules to 1. That\n * number is a cost claim as much as a\n * correctness one: shipping a document over a network hop is only sensible if the\n * document is not the entire design system plus Storybook's chrome.\n */\nexport interface RenderDocument {\n readonly documentVersion: 1;\n readonly subject: SubjectRef;\n\n /**\n * The subject subtree, serialized.\n *\n * Serialized rather than referenced by URL: a URL makes the renderer\n * responsible for reproducing the application's state, which it cannot do and\n * should not try. What is sent is the markup that existed at the moment of\n * capture, which is the only definition of \"this render\" that survives a hop.\n */\n readonly html: string;\n\n /**\n * The ancestor context the subject was rendered inside.\n *\n * Not decoration. Pruning keeps rules like `html.dark .card` and\n * `.app .list > li`, whose left-hand side lives *above* the subject — render\n * the subtree bare and those rules match nothing, so the image is missing\n * exactly the styling the collector went to the trouble of proving applies.\n *\n * Reproduced as empty open tags rather than by re-rendering the application:\n * what a selector needs is a chain of elements with the right tags, ids,\n * classes, and attributes, and nothing above the subject contributes anything\n * else to it that {@link RenderDocument.inherited} does not already carry.\n */\n readonly frame: RenderFrame;\n\n /**\n * Applicable stylesheet text, in cascade order.\n *\n * Order is load-bearing and is the caller's to preserve — the cascade breaks\n * specificity ties by document order, so a set that arrives shuffled paints\n * differently while hashing the same if this were a set rather than a list.\n */\n readonly css: readonly string[];\n\n readonly viewport: Viewport;\n\n /**\n * Inherited values in force at the subject root, applied to its wrapper.\n *\n * The same requirement as `RawCapture.inheritedSeed` and for the same reason:\n * pruning drops rules on ancestors outside the subtree, and a font-size\n * inherited from `<html>` is not optional decoration — dropping it changes\n * every metric in the image.\n */\n readonly inherited: Readonly<Record<string, string>>;\n\n /**\n * Fonts the acquiring side observed as loaded.\n *\n * Carried, not resolved. A renderer that lacks one of these will substitute\n * and produce different metrics, and that has to be *detectable* rather than\n * silently absorbed — see {@link RenderIdentity}.\n */\n readonly fonts: readonly string[];\n\n /** External references, url → content hash. Present so a swap is visible. */\n readonly assets?: Readonly<Record<string, string>>;\n\n /**\n * Base against which relative resource references were resolved.\n *\n * Preserves relative-reference resolution for either document kind. Resource\n * closure is denoted by `resources`, not by the presence of this field.\n */\n readonly baseUrl?: string;\n\n /**\n * Immutable responses needed to paint this document, keyed by absolute URL.\n *\n * Presence — including an empty object — means the document is\n * resource-closed: a renderer must not consult the network for a missing URL.\n * Absence preserves the older network-capable document contract and must not\n * be described as portable to another machine.\n */\n readonly resources?: Readonly<Record<string, RenderResource>>;\n\n readonly diagnostics: readonly Diagnostic[];\n}\n\nexport interface RenderResource {\n readonly contentType: string;\n /** Response bytes, base64. */\n readonly bytes: string;\n readonly digest: Digest;\n\n /**\n * The status a renderer answers this URL with. Absent means 200.\n *\n * A subject is allowed to reference something that is not there: a fallback\n * test does it on purpose, and the browser it was acquired in painted the\n * broken state rather than the image. Recording the failure as a failure is\n * what lets a renderer reproduce that state without asking the network.\n */\n readonly status?: number;\n}\n\nexport interface RenderFrame {\n /** Attributes on `<html>`. `class=\"dark\"` here decides half a design system. */\n readonly html: Readonly<Record<string, string>>;\n readonly body: Readonly<Record<string, string>>;\n /** Elements between `<body>` and the subject root, outermost first. */\n readonly ancestors: readonly FrameElement[];\n\n /**\n * Used `width` of the subject's parent, in CSS pixels and in the parent's own\n * `box-sizing`, when layout was observable. Written back on the same box, it\n * gives the content box a percentage on the subject resolves against.\n *\n * A subject declaring `width: 100%` is a different size in a 1024px page and\n * inside a 600px panel, and nothing else in this document says which it was.\n * Absent under a profile with no layout engine, in which case the render falls\n * back to the viewport and the difference, if any, is real rather than hidden.\n */\n readonly containerWidth?: number;\n}\n\nexport interface FrameElement {\n readonly tag: string;\n readonly attributes: Readonly<Record<string, string>>;\n}\n\n/**\n * Content address of a document: *what is to be painted*.\n *\n * This is the deferral lever. Rendering is the expensive phase, and a document\n * whose digest has been rendered before under the same identity has an image\n * already — so the cheapest render is the one that is skipped. Nothing about the\n * branch, the commit, or the file path enters this (Principle 4), so moving a\n * story between files does not cost a re-render.\n *\n * Deliberately covers `viewport` in full, `deviceScaleFactor` included: unlike\n * the semantic key (ADR-0010), a raster genuinely differs at 2x and the whole\n * purpose of this digest is to address the raster.\n *\n * **Limit, stated rather than discovered.** Declaration values arrive serialized\n * by the acquiring engine's CSSOM — jsdom writes `rgb(18, 52, 86)` where the\n * author wrote `#123456` — and this phase deliberately does not canonicalize\n * them, because a collector that normalizes is a second ruleset versioned by\n * nothing (ADR-0001). So two engines can address the same page differently. That\n * makes a render cache *per acquiring engine*, which is a missed cache hit and\n * never a wrong image.\n */\nexport function documentDigest(document: RenderDocument): Digest {\n const origin = pageOrigin(document.baseUrl);\n return digestValue({\n documentVersion: document.documentVersion,\n html: document.html,\n frame: {\n html: { ...document.frame.html },\n body: { ...document.frame.body },\n ancestors: document.frame.ancestors.map((element) => ({\n tag: element.tag,\n attributes: { ...element.attributes },\n })),\n ...(document.frame.containerWidth !== undefined\n ? { containerWidth: document.frame.containerWidth }\n : {}),\n },\n css: [...document.css],\n viewport: { ...document.viewport },\n inherited: { ...document.inherited },\n fonts: [...document.fonts],\n ...(document.assets ? { assets: { ...document.assets } } : {}),\n // The base and the page's own resources as paths: a collector serves the\n // build on a free port, and a digest holding it would miss the render cache\n // on every run. The document keeps both absolute, because the renderer\n // resolves against them; a resource another origin serves stays absolute.\n ...(document.baseUrl !== undefined ? { baseUrl: withoutOrigin(document.baseUrl, origin) } : {}),\n ...(document.resources !== undefined\n ? {\n resources: Object.fromEntries(\n Object.entries(document.resources).map(([url, resource]) => [\n withoutOrigin(url, origin),\n { ...resource },\n ]),\n ),\n }\n : {}),\n });\n}\n\n/**\n * Who painted it.\n *\n * The durable half of this system rests on one requirement — *images compared\n * against each other must come from the same machine* — and a requirement that\n * is merely stated gets violated by a CI runner upgrade nobody announced. This\n * makes it a value, so it can be compared rather than assumed.\n *\n * Every field is something that has been observed to move pixels without moving\n * markup. None of them is a guess about what *might* matter.\n */\nexport interface RenderIdentity {\n /** `playwright-chromium@1.49.0`, `remote:render.internal`, … */\n readonly renderer: string;\n /** Engine build. A Chromium bump repaints text; that is not a regression. */\n readonly engine: string;\n /** OS and architecture. Font rasterization differs across both. */\n readonly platform: string;\n readonly deviceScaleFactor: number;\n /**\n * Fonts the *renderer* actually has.\n *\n * Compared against {@link RenderDocument.fonts} at render time, because a\n * substituted font is the single most common way two machines disagree, and\n * it is invisible in every artifact except the image itself.\n */\n readonly fonts: readonly string[];\n\n /**\n * Digest of what the renderer did *to* the page so it could be observed.\n *\n * Pausing animations, hiding a caret, waiting for fonts — each is necessary\n * and each widens the gap between what was measured and what a person sees.\n * The gap itself is acceptable; a *difference* in it between two runs is not,\n * because the images then differ for a reason that is not the code and the\n * report blames whichever component sits under the pixels. Folding it in here\n * names that gap as the cause of the change instead, by the same mechanism\n * that names a moved machine.\n *\n * Optional only because a renderer may predate the field. Absent means the\n * renderer did not record what it did, which is emphatically not the same as\n * \"it did nothing\" — and two such renderers will compare, which is the hazard\n * that argues for every renderer setting it.\n */\n readonly stabilization?: Digest;\n\n /**\n * Digest of pixel-affecting browser launch and raster settings.\n *\n * Separate from stabilization: one describes how the browser paints, the\n * other what was done to the page before it was read.\n */\n readonly rasterization?: Digest;\n}\n\nexport function identityDigest(identity: RenderIdentity): Digest {\n return digestValue({\n renderer: identity.renderer,\n engine: identity.engine,\n platform: identity.platform,\n ...(identity.stabilization !== undefined ? { stabilization: identity.stabilization } : {}),\n ...(identity.rasterization !== undefined ? { rasterization: identity.rasterization } : {}),\n deviceScaleFactor: identity.deviceScaleFactor,\n fonts: [...identity.fonts],\n });\n}\n\n/**\n * A rendered image and the conditions it was produced under.\n *\n * `bytes` is base64 rather than a `Buffer` so the whole value survives the same\n * hops the document does. A raster that could only exist in Node would make the\n * remote route a special case in every function that touches one.\n *\n * The image is optional, and the three fields that describe it move together.\n * A subject that occupies no pixels — a wrapper whose only child went to a\n * portal, a `describeConformance` mount with no children — still has a\n * document, rules, components and an accessibility tree, all of which compare.\n * Refusing to photograph it is right; refusing the subject reported a quarter\n * of Material UI's unit tier as unobserved over an axis nothing was asking\n * about. Absent here means *this subject has no pixels to observe*, which is a\n * measurement; it never means *the image was lost*.\n */\nexport interface Raster {\n readonly documentDigest: Digest;\n readonly identity: RenderIdentity;\n /**\n * Device pixels. `width / viewport.width` is the scale, and is asserted.\n *\n * Absent exactly when {@link bytes} is, and {@link pictured} is the one place\n * that reads the three together.\n */\n readonly width?: number;\n readonly height?: number;\n /** PNG, base64. Absent when the subject occupies no pixels. */\n readonly bytes?: string;\n /** Fonts the document declared that the renderer did not have. */\n readonly missingFonts: readonly string[];\n\n /**\n * The browser-computed accessibility tree from the acquisition that produced\n * this candidate. It is independent evidence: pixel equality cannot settle it.\n * Absent means the acquisition host did not observe this boundary.\n */\n readonly accessibility?: import('./accessibility.js').AccessibilitySnapshot;\n\n /**\n * What the document that painted this said about its own components (ADR-0018).\n *\n * The field that makes a baseline self-describing, and the reason it is here\n * rather than only in a history store. Separating the component that *caused* a\n * change from the components the change merely moved needs both revisions, and\n * a stored baseline is an image: without this, a run has one document and ranks\n * by area — which\n * [journal 0013](https://github.com/Variance-Authority/variance-authority/blob/main/docs/context/journal/0013-observability.md)\n * measured as backwards.\n *\n * Text, and small: one line per component boundary. It rides in the sidecar\n * beside a PNG that dominates it, so a baseline carries its own explanation\n * wherever it is copied, and a store or a transport that drops it degrades to\n * ranking by area rather than to a wrong answer.\n *\n * Optional because a baseline written before this existed does not have it, and\n * because a profile or a collector that supplies no snapshot cannot produce it.\n * Absent means *unknown*, never *no components*.\n */\n readonly components?: readonly ComponentHash[];\n\n /**\n * What inspection found in the document that painted this, as marks.\n *\n * The field that lets a later run say *this defect was already here* instead of\n * printing every defect it finds with no indication of when it arrived. A\n * finding is produced from one render with no baseline consulted — that is the\n * point of having one — and the consequence is that the list is identical on\n * the run that introduced a defect and on the two hundred runs after it. A\n * reviewer reading `a control inside another control` has no way to tell an\n * inherited condition from something they just wrote, and the two are different\n * decisions.\n *\n * Marks rather than findings, from {@link findingMark}. The sentence, the\n * landmark phrase and the source line are all rewritten by edits that do not\n * touch the defect, so storing them would report a defect as new every time the\n * copy beside it changed — and they are the bulk of the bytes, in a sidecar\n * that is committed beside every baseline.\n *\n * **Absent means nothing inspected it, never that it was clean.** A baseline\n * written before this existed, or by a collector that supplies no snapshot, has\n * no list, and a run that read absence as `[]` would announce every standing\n * defect in the suite as newly introduced on the first run after an upgrade.\n */\n readonly findingMarks?: readonly string[];\n\n /**\n * The semantic snapshot of the document that painted this, packed.\n *\n * The region join needs to know where each element was in the render the\n * pixels are compared against. A changed region is the union of what left and\n * what arrived, and a stored baseline is an image: without its own tree, a\n * control replaced by a larger element changes pixels outside every box this\n * render has, and the join names the row around it. With it, the join pairs\n * the two renders' nodes with the matcher the semantic diff reads, so a\n * region and a delta agree about which element a node was.\n *\n * Packed (see {@link PackedSnapshot}) because the tree is an order of\n * magnitude larger than the component hashes, and it rides in every sidecar.\n * Every store and transport carries the text as it is, and only the run that\n * decides unpacks it.\n *\n * **Absent means not recorded.** A baseline written before this existed, or\n * from a collector that supplied no snapshot, has none, and a verdict whose\n * regions were joined without it says so.\n */\n readonly snapshot?: PackedSnapshot;\n}\n\n/**\n * A raster whose image is in hand.\n *\n * Every pixel comparison in the codebase needs all three of `bytes`, `width`\n * and `height`, and they are absent together or present together. Narrowing\n * them one at a time is how a caller ends up reading the bytes of a subject\n * that was never photographed, so the narrowing lives here and is done once.\n */\nexport type Pictured = Raster & {\n readonly width: number;\n readonly height: number;\n readonly bytes: string;\n};\n\n/** Whether this record carries an image, narrowing all three fields together. */\nexport function pictured(raster: Raster): raster is Pictured {\n return raster.bytes !== undefined && raster.width !== undefined && raster.height !== undefined;\n}\n\n/**\n * Whether the subject occupied any pixels, answered without the image.\n *\n * The question {@link pictured} cannot be asked of a sidecar. Bytes live in a\n * separate file, row or object, so a record read without them carries the width\n * and the height and nothing else — and those *are* the store's record of\n * whether there was ever anything to photograph, which is why `sidecarFrom`\n * refuses a sidecar carrying one of them.\n *\n * So: this for a record, {@link pictured} for an image in hand. A caller that\n * reached for `pictured` here would read every sidecar in the store as a subject\n * with no pixels, because none of them have bytes.\n */\nexport function occupiesPixels(raster: Omit<Raster, 'bytes'>): boolean {\n return raster.width !== undefined && raster.height !== undefined;\n}\n\n/**\n * The image, or a refusal naming the subject that has none.\n *\n * For the paths where a missing image is a programming error rather than a\n * measurement — a diff, a PNG encode — so they read as assertions instead of\n * as a non-null assertion nobody can audit.\n */\nexport function picture(raster: Raster, subject: string): Pictured {\n if (!pictured(raster)) {\n throw new Error(\n `\\`${subject}\\` occupies no pixels, and this path needs an image; ` +\n 'a subject with no raster compares on its document alone',\n );\n }\n return raster;\n}\n"]}
@@ -0,0 +1,191 @@
1
+ import type { DriverTarget, Intervention } from './intervention.js';
2
+ /**
3
+ * The tricks a stylesheet cannot reach, and the one call each makes of a driver.
4
+ *
5
+ * Every other trick in [`stabilize.ts`](./stabilize.ts) is CSS, a screenshot
6
+ * option or a script the page runs on itself. Three causes of a page that reads
7
+ * two ways sit outside the document:
8
+ *
9
+ * - **the motion preference** — a page that honours `prefers-reduced-motion`
10
+ * runs its script animation only when the browser says motion is welcome, and
11
+ * a stylesheet cannot reach a `requestAnimationFrame` loop;
12
+ * - **the clock** — a page that prints `new Date()` prints a new value every
13
+ * run, and the date is read by the page's first script, before any stylesheet
14
+ * or settle step exists, so it is replaced by a script the browser runs before
15
+ * the page's own;
16
+ * - **the pointer** — a hover ends when the pointer leaves, and nothing in the
17
+ * page can move it.
18
+ *
19
+ * So each is declared as a {@link DriverState} and applied here, through the
20
+ * structural {@link DriverTarget} a Playwright `Page` already is. A host that
21
+ * owns the browser calls {@link driveBeforeLoad} before its first navigation
22
+ * and hands the page the ids it applied. A host that shares the page with
23
+ * somebody who moves the pointer, such as a test, also calls
24
+ * {@link driveBeforeRead} before each subject is read. A host that does not — a page agent, a test runner's own
25
+ * browser — drives nothing, and {@link forDriver} keeps those tricks out of its
26
+ * recorded recipe, so the absence shows in the reading's ids and digest the way
27
+ * a trick filtered by `needs` does.
28
+ */
29
+ /**
30
+ * The instant a fixed clock reads unless a project names another.
31
+ *
32
+ * Noon UTC, so it is the same calendar date in every zone from UTC−11 to
33
+ * UTC+11: a page that prints today's date prints one date whichever zone the
34
+ * browser runs in. A date in the past, so a page that compares against "now"
35
+ * to show something as expired does so on every run or on none.
36
+ */
37
+ export declare const DEFAULT_NOW = "2024-06-15T12:00:00.000Z";
38
+ /**
39
+ * The milliseconds an ISO 8601 instant names, or an error that says what an
40
+ * instant is.
41
+ *
42
+ * The zone is required. `2024-06-15T12:00` with no zone is read in whatever
43
+ * zone the machine runs in, so two machines would fix two clocks — the
44
+ * variance this trick exists to remove.
45
+ */
46
+ export declare function instantOf(now: string): number;
47
+ /**
48
+ * `prefers-reduced-motion: reduce`, the switch a well-built page already reads.
49
+ *
50
+ * It reaches what `pin-animations` cannot: a script animation, which a
51
+ * stylesheet has no handle on. It reaches only a page that honours the
52
+ * preference, and that is the cost: the page is read as a user who asked for
53
+ * less motion sees it, which can differ from what everybody else sees.
54
+ */
55
+ export declare const reducedMotion: Intervention;
56
+ /**
57
+ * `Date` reads one instant; timers, animation frames and `performance` keep
58
+ * running.
59
+ *
60
+ * An init script, {@link freezeDate}, run in every frame before the page's own
61
+ * first script. It replaces `Date` and nothing else, because the thing that
62
+ * prints two ways is the date a page shows, and the things that make a page
63
+ * settle are timers and frames. Playwright's `clock.setFixedTime` was measured
64
+ * against it and rejected: it installs a fake for `setTimeout`,
65
+ * `requestAnimationFrame` and `performance.now` as well, and a frame's
66
+ * timestamp no longer agrees with `document.timeline`, which drives every CSS
67
+ * and Web Animations frame. Installing no clock also leaves `page.clock` to the
68
+ * test that wants one.
69
+ *
70
+ * The id carries the instant when it is not {@link DEFAULT_NOW}, as
71
+ * `freeze-clock@2031-01-02T03:04:05.000Z`, so the instant is in the recipe's
72
+ * digest and so in the environment key: a baseline read at one instant and a
73
+ * run read at another are compared, and the instant is named as the cause of
74
+ * whatever changed.
75
+ *
76
+ * The cost: code that measures elapsed time with `Date.now()`, such as a
77
+ * debounce, sees no time pass and never fires. `performance.now()` keeps
78
+ * counting.
79
+ */
80
+ export declare function freezeClockAt(now: string): Intervention;
81
+ /** {@link freezeClockAt} the default instant, {@link DEFAULT_NOW}. */
82
+ export declare const freezeClock: Intervention;
83
+ /**
84
+ * The clock trick a `freeze-clock@<instant>` id names, or undefined for any
85
+ * other id.
86
+ *
87
+ * The registry holds `freeze-clock` alone; an id with an instant in it is
88
+ * resolved here, so a recipe that crossed into a page as ids keeps its instant.
89
+ */
90
+ export declare function clockById(id: string): Intervention | undefined;
91
+ /**
92
+ * The init script {@link freezeClock} installs: `Date` answers `frozen`, and
93
+ * nothing else changes.
94
+ *
95
+ * Runs in the page, so it closes over nothing. `new Date()`, `Date()` and
96
+ * `Date.now()` read the instant; `new Date(value)`, `Date.UTC`, `Date.parse`
97
+ * and every method on a date are the browser's own. `Intl.DateTimeFormat`'s
98
+ * `format()` and `formatToParts()` with no date read the browser's clock rather
99
+ * than `Date`, so they are given the instant too.
100
+ */
101
+ export declare function freezeDate(frozen: number): void;
102
+ /**
103
+ * The pointer moved off the page, so nothing is hovered.
104
+ *
105
+ * To `(-1, -1)` rather than to the page's corner: content at the top left is
106
+ * under `(0, 0)` and stays hovered there. `pointer-events: none` is not a
107
+ * substitute — measured, an element already hovered keeps `:hover` under it.
108
+ *
109
+ * Once per page, before load, where nothing but the host moves the pointer. A
110
+ * move is acknowledged on the next frame, ~14 ms measured, so a host that alone
111
+ * drives its page does not move it again. A host that shares the page with a
112
+ * test does, before every reading: see {@link driveBeforeRead}.
113
+ */
114
+ export declare const noHover: Intervention;
115
+ /**
116
+ * Nothing focused, so a field a test filled or a page focused on load reads
117
+ * as it does untouched.
118
+ *
119
+ * A settle step rather than a stylesheet: `:focus` and `:focus-visible` follow
120
+ * the element that holds focus, and no rule can unmatch them. It blurs the
121
+ * deepest focused element, walking through shadow roots, because a shadow host
122
+ * is the document's `activeElement` for whatever its tree focused, and blurring
123
+ * the host leaves the input inside it focused and outlined.
124
+ *
125
+ * The cost: a page evaluate per subject, and an animation frame as well when
126
+ * the document holds an `[autofocus]`. A host that reads a page somebody goes
127
+ * on using — a test, an agent's browser — gives focus back once the reading
128
+ * is taken; a host that owns its page does not need to.
129
+ */
130
+ export declare const noFocus: Intervention;
131
+ /**
132
+ * The tricks that put a page into the state nobody is touching, in every
133
+ * default collection recipe whose tier can see them.
134
+ *
135
+ * A hover, a focus, a script animation and a printed date are each a second
136
+ * reading of one subject, and each comes from something other than the code
137
+ * under review. Three of them reach the page only through a driver, so a host
138
+ * that cannot drive one leaves it out of the ids it records — see {@link forDriver}.
139
+ */
140
+ export declare const UNTOUCHED: readonly Intervention[];
141
+ /**
142
+ * The media features a recipe's driver tricks make the browser report, merged
143
+ * under the ones a project declared, or undefined when there are none.
144
+ *
145
+ * Collection resolves `@media` against declared features, not against the
146
+ * browser, so a preference the driver emulated and nobody declared would be
147
+ * read as unknown: both the `reduce` and the `no-preference` block applied, a
148
+ * box the browser never drew. Declaring what the trick emulates gives the
149
+ * preference one owner. A project's own value wins, and
150
+ * {@link driveBeforeLoad} emulates that value, so the two still agree.
151
+ */
152
+ export declare function featuresOf(recipe: readonly Intervention[], declared?: Readonly<Record<string, string>>): Readonly<Record<string, string>> | undefined;
153
+ /**
154
+ * Apply what a recipe asks of the browser before the page loads.
155
+ *
156
+ * All three survive navigation, so a host calls this once per page, before its
157
+ * first `goto` or `setContent`. Returns the ids applied, in recipe order. The
158
+ * clock's instant is the trick's own, so a host that wants another instant
159
+ * asks `recipeToDrive` for the recipe rather than telling this.
160
+ *
161
+ * `declared` is the media features the project declared. A declared
162
+ * `prefers-reduced-motion` is what the motion trick emulates, so the browser
163
+ * reports what the record says; see {@link featuresOf}.
164
+ */
165
+ export declare function driveBeforeLoad(recipe: readonly Intervention[], target: DriverTarget, declared?: Readonly<Record<string, string>>): Promise<readonly string[]>;
166
+ /**
167
+ * Take the pointer off the page again before a subject is read.
168
+ *
169
+ * For a host that shares the page: a test hovers a menu to open it and then
170
+ * observes, so the pointer is wherever the test's last step left it. A host
171
+ * that alone drives its page needs only {@link driveBeforeLoad}.
172
+ *
173
+ * Every reading pays the frame, hovered or not: measured in Chromium, a
174
+ * Playwright hover restyles the element while `:hover` matches nothing from
175
+ * script, so the page cannot say whether the move is needed. The pointer is
176
+ * not put back afterwards, and the move fires `mouseleave`: a menu a script
177
+ * opened on hover is closed for the test's next step.
178
+ */
179
+ export declare function driveBeforeRead(recipe: readonly Intervention[], target: Pick<DriverTarget, 'mouse'>): Promise<readonly string[]>;
180
+ /**
181
+ * The recipe as it holds on a page, given the driver tricks a host applied.
182
+ *
183
+ * A trick with no `driver` is kept. A trick with one is kept only when its id
184
+ * is in `driven`: a host that could not drive it leaves it out of the ids and
185
+ * the digest it records, rather than claiming a hold the page never had. The
186
+ * clock is the one trick whose id the driver may change, because the instant is
187
+ * the host's to choose: the recipe's clock is recorded as the one the host
188
+ * fixed.
189
+ */
190
+ export declare function forDriver(recipe: readonly Intervention[], driven: readonly string[]): readonly Intervention[];
191
+ //# sourceMappingURL=drive.d.ts.map
@@ -0,0 +1,343 @@
1
+ /**
2
+ * The tricks a stylesheet cannot reach, and the one call each makes of a driver.
3
+ *
4
+ * Every other trick in [`stabilize.ts`](./stabilize.ts) is CSS, a screenshot
5
+ * option or a script the page runs on itself. Three causes of a page that reads
6
+ * two ways sit outside the document:
7
+ *
8
+ * - **the motion preference** — a page that honours `prefers-reduced-motion`
9
+ * runs its script animation only when the browser says motion is welcome, and
10
+ * a stylesheet cannot reach a `requestAnimationFrame` loop;
11
+ * - **the clock** — a page that prints `new Date()` prints a new value every
12
+ * run, and the date is read by the page's first script, before any stylesheet
13
+ * or settle step exists, so it is replaced by a script the browser runs before
14
+ * the page's own;
15
+ * - **the pointer** — a hover ends when the pointer leaves, and nothing in the
16
+ * page can move it.
17
+ *
18
+ * So each is declared as a {@link DriverState} and applied here, through the
19
+ * structural {@link DriverTarget} a Playwright `Page` already is. A host that
20
+ * owns the browser calls {@link driveBeforeLoad} before its first navigation
21
+ * and hands the page the ids it applied. A host that shares the page with
22
+ * somebody who moves the pointer, such as a test, also calls
23
+ * {@link driveBeforeRead} before each subject is read. A host that does not — a page agent, a test runner's own
24
+ * browser — drives nothing, and {@link forDriver} keeps those tricks out of its
25
+ * recorded recipe, so the absence shows in the reading's ids and digest the way
26
+ * a trick filtered by `needs` does.
27
+ */
28
+ /**
29
+ * The instant a fixed clock reads unless a project names another.
30
+ *
31
+ * Noon UTC, so it is the same calendar date in every zone from UTC−11 to
32
+ * UTC+11: a page that prints today's date prints one date whichever zone the
33
+ * browser runs in. A date in the past, so a page that compares against "now"
34
+ * to show something as expired does so on every run or on none.
35
+ */
36
+ export const DEFAULT_NOW = '2024-06-15T12:00:00.000Z';
37
+ const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;
38
+ /**
39
+ * The milliseconds an ISO 8601 instant names, or an error that says what an
40
+ * instant is.
41
+ *
42
+ * The zone is required. `2024-06-15T12:00` with no zone is read in whatever
43
+ * zone the machine runs in, so two machines would fix two clocks — the
44
+ * variance this trick exists to remove.
45
+ */
46
+ export function instantOf(now) {
47
+ const time = INSTANT.test(now) ? Date.parse(now) : Number.NaN;
48
+ if (Number.isNaN(time)) {
49
+ throw new Error('`now` must be an ISO 8601 instant with its zone, such as "2024-06-15T12:00:00Z" or ' +
50
+ `"2024-06-15T14:00:00+02:00", not ${JSON.stringify(now)}`);
51
+ }
52
+ return time;
53
+ }
54
+ /**
55
+ * `prefers-reduced-motion: reduce`, the switch a well-built page already reads.
56
+ *
57
+ * It reaches what `pin-animations` cannot: a script animation, which a
58
+ * stylesheet has no handle on. It reaches only a page that honours the
59
+ * preference, and that is the cost: the page is read as a user who asked for
60
+ * less motion sees it, which can differ from what everybody else sees.
61
+ */
62
+ export const reducedMotion = {
63
+ id: 'reduced-motion',
64
+ trick: 'reset',
65
+ needs: 'layout',
66
+ governs: 'motion-preference',
67
+ because: 'the browser reports prefers-reduced-motion: reduce, so a page that honours it stops its script animations',
68
+ driver: { reducedMotion: 'reduce' },
69
+ };
70
+ const CLOCK_ID = 'freeze-clock';
71
+ /**
72
+ * `Date` reads one instant; timers, animation frames and `performance` keep
73
+ * running.
74
+ *
75
+ * An init script, {@link freezeDate}, run in every frame before the page's own
76
+ * first script. It replaces `Date` and nothing else, because the thing that
77
+ * prints two ways is the date a page shows, and the things that make a page
78
+ * settle are timers and frames. Playwright's `clock.setFixedTime` was measured
79
+ * against it and rejected: it installs a fake for `setTimeout`,
80
+ * `requestAnimationFrame` and `performance.now` as well, and a frame's
81
+ * timestamp no longer agrees with `document.timeline`, which drives every CSS
82
+ * and Web Animations frame. Installing no clock also leaves `page.clock` to the
83
+ * test that wants one.
84
+ *
85
+ * The id carries the instant when it is not {@link DEFAULT_NOW}, as
86
+ * `freeze-clock@2031-01-02T03:04:05.000Z`, so the instant is in the recipe's
87
+ * digest and so in the environment key: a baseline read at one instant and a
88
+ * run read at another are compared, and the instant is named as the cause of
89
+ * whatever changed.
90
+ *
91
+ * The cost: code that measures elapsed time with `Date.now()`, such as a
92
+ * debounce, sees no time pass and never fires. `performance.now()` keeps
93
+ * counting.
94
+ */
95
+ export function freezeClockAt(now) {
96
+ const at = instantOf(now);
97
+ return {
98
+ id: at === instantOf(DEFAULT_NOW) ? CLOCK_ID : `${CLOCK_ID}@${new Date(at).toISOString()}`,
99
+ trick: 'hold',
100
+ needs: 'layout',
101
+ governs: 'clock',
102
+ because: 'Date reads one fixed instant, so elapsed time measured with Date.now() reads zero',
103
+ driver: { clock: { at } },
104
+ };
105
+ }
106
+ /** {@link freezeClockAt} the default instant, {@link DEFAULT_NOW}. */
107
+ export const freezeClock = freezeClockAt(DEFAULT_NOW);
108
+ /**
109
+ * The clock trick a `freeze-clock@<instant>` id names, or undefined for any
110
+ * other id.
111
+ *
112
+ * The registry holds `freeze-clock` alone; an id with an instant in it is
113
+ * resolved here, so a recipe that crossed into a page as ids keeps its instant.
114
+ */
115
+ export function clockById(id) {
116
+ return id.startsWith(`${CLOCK_ID}@`) ? freezeClockAt(id.slice(CLOCK_ID.length + 1)) : undefined;
117
+ }
118
+ /**
119
+ * The init script {@link freezeClock} installs: `Date` answers `frozen`, and
120
+ * nothing else changes.
121
+ *
122
+ * Runs in the page, so it closes over nothing. `new Date()`, `Date()` and
123
+ * `Date.now()` read the instant; `new Date(value)`, `Date.UTC`, `Date.parse`
124
+ * and every method on a date are the browser's own. `Intl.DateTimeFormat`'s
125
+ * `format()` and `formatToParts()` with no date read the browser's clock rather
126
+ * than `Date`, so they are given the instant too.
127
+ */
128
+ export function freezeDate(frozen) {
129
+ // TODO: `Temporal.Now` and a worker's `Date` read the real clock. This runs
130
+ // in each frame's window and replaces `Date` alone, so a page that prints a
131
+ // date from either prints a second reading.
132
+ const Real = Date;
133
+ function Frozen(...args) {
134
+ if (new.target === undefined)
135
+ return new Real(frozen).toString();
136
+ return Reflect.construct(Real, args.length === 0 ? [frozen] : args, new.target);
137
+ }
138
+ Object.setPrototypeOf(Frozen, Real);
139
+ Object.defineProperties(Frozen, {
140
+ prototype: { value: Real.prototype },
141
+ name: { value: 'Date' },
142
+ length: { value: 7 },
143
+ now: { value: () => frozen, writable: true, configurable: true },
144
+ });
145
+ Object.defineProperty(Real.prototype, 'constructor', { value: Frozen, writable: true, configurable: true });
146
+ Object.defineProperty(globalThis, 'Date', { value: Frozen, writable: true, configurable: true });
147
+ const format = Intl.DateTimeFormat.prototype;
148
+ const formatting = Object.getOwnPropertyDescriptor(format, 'format')?.get;
149
+ if (formatting !== undefined) {
150
+ Object.defineProperty(format, 'format', {
151
+ configurable: true,
152
+ get() {
153
+ const bound = formatting.call(this);
154
+ return (date) => bound(date === undefined ? frozen : date);
155
+ },
156
+ });
157
+ }
158
+ const toParts = format.formatToParts;
159
+ Object.defineProperty(format, 'formatToParts', {
160
+ configurable: true,
161
+ writable: true,
162
+ value(date) {
163
+ return toParts.call(this, date === undefined ? frozen : date);
164
+ },
165
+ });
166
+ }
167
+ /**
168
+ * The pointer moved off the page, so nothing is hovered.
169
+ *
170
+ * To `(-1, -1)` rather than to the page's corner: content at the top left is
171
+ * under `(0, 0)` and stays hovered there. `pointer-events: none` is not a
172
+ * substitute — measured, an element already hovered keeps `:hover` under it.
173
+ *
174
+ * Once per page, before load, where nothing but the host moves the pointer. A
175
+ * move is acknowledged on the next frame, ~14 ms measured, so a host that alone
176
+ * drives its page does not move it again. A host that shares the page with a
177
+ * test does, before every reading: see {@link driveBeforeRead}.
178
+ */
179
+ export const noHover = {
180
+ id: 'no-hover',
181
+ trick: 'reset',
182
+ needs: 'layout',
183
+ governs: 'pointer',
184
+ because: 'the pointer moved off the page, so a hover a test left behind is not read',
185
+ driver: { pointer: 'outside' },
186
+ };
187
+ /**
188
+ * Nothing focused, so a field a test filled or a page focused on load reads
189
+ * as it does untouched.
190
+ *
191
+ * A settle step rather than a stylesheet: `:focus` and `:focus-visible` follow
192
+ * the element that holds focus, and no rule can unmatch them. It blurs the
193
+ * deepest focused element, walking through shadow roots, because a shadow host
194
+ * is the document's `activeElement` for whatever its tree focused, and blurring
195
+ * the host leaves the input inside it focused and outlined.
196
+ *
197
+ * The cost: a page evaluate per subject, and an animation frame as well when
198
+ * the document holds an `[autofocus]`. A host that reads a page somebody goes
199
+ * on using — a test, an agent's browser — gives focus back once the reading
200
+ * is taken; a host that owns its page does not need to.
201
+ */
202
+ export const noFocus = {
203
+ id: 'no-focus',
204
+ trick: 'reset',
205
+ needs: 'layout',
206
+ governs: 'focus',
207
+ because: 'focus released, so a field a test or the page focused is read unfocused',
208
+ // Again in a frame callback when the document holds an `autofocus`, because
209
+ // the browser honours one at the page's next rendering update: a blur before
210
+ // it is undone, and a document written by `setContent` has had no update yet.
211
+ // Only then, so a page without one pays no frame per subject.
212
+ settle: async (target) => {
213
+ await target.evaluate(() => new Promise((resolve) => {
214
+ const view = globalThis;
215
+ const release = () => {
216
+ let focused = view.document.activeElement;
217
+ while (focused?.shadowRoot?.activeElement != null)
218
+ focused = focused.shadowRoot.activeElement;
219
+ focused?.blur?.();
220
+ };
221
+ release();
222
+ if (view.document.querySelector('[autofocus]') === null)
223
+ return resolve();
224
+ view.requestAnimationFrame(() => {
225
+ release();
226
+ resolve();
227
+ });
228
+ }));
229
+ },
230
+ };
231
+ /**
232
+ * The tricks that put a page into the state nobody is touching, in every
233
+ * default collection recipe whose tier can see them.
234
+ *
235
+ * A hover, a focus, a script animation and a printed date are each a second
236
+ * reading of one subject, and each comes from something other than the code
237
+ * under review. Three of them reach the page only through a driver, so a host
238
+ * that cannot drive one leaves it out of the ids it records — see {@link forDriver}.
239
+ */
240
+ export const UNTOUCHED = [noHover, noFocus, reducedMotion, freezeClock];
241
+ const MOTION = 'prefers-reduced-motion';
242
+ /**
243
+ * The media features a recipe's driver tricks make the browser report, merged
244
+ * under the ones a project declared, or undefined when there are none.
245
+ *
246
+ * Collection resolves `@media` against declared features, not against the
247
+ * browser, so a preference the driver emulated and nobody declared would be
248
+ * read as unknown: both the `reduce` and the `no-preference` block applied, a
249
+ * box the browser never drew. Declaring what the trick emulates gives the
250
+ * preference one owner. A project's own value wins, and
251
+ * {@link driveBeforeLoad} emulates that value, so the two still agree.
252
+ */
253
+ export function featuresOf(recipe, declared) {
254
+ const motion = recipe.find((intervention) => intervention.driver?.reducedMotion !== undefined);
255
+ const features = {
256
+ ...(motion === undefined ? {} : { [MOTION]: motion.driver.reducedMotion }),
257
+ ...declared,
258
+ };
259
+ return Object.keys(features).length === 0 ? undefined : features;
260
+ }
261
+ /**
262
+ * Apply what a recipe asks of the browser before the page loads.
263
+ *
264
+ * All three survive navigation, so a host calls this once per page, before its
265
+ * first `goto` or `setContent`. Returns the ids applied, in recipe order. The
266
+ * clock's instant is the trick's own, so a host that wants another instant
267
+ * asks `recipeToDrive` for the recipe rather than telling this.
268
+ *
269
+ * `declared` is the media features the project declared. A declared
270
+ * `prefers-reduced-motion` is what the motion trick emulates, so the browser
271
+ * reports what the record says; see {@link featuresOf}.
272
+ */
273
+ export async function driveBeforeLoad(recipe, target, declared) {
274
+ const applied = [];
275
+ for (const intervention of recipe) {
276
+ const driver = intervention.driver;
277
+ if (driver?.pointer === 'outside') {
278
+ await target.mouse.move(-1, -1);
279
+ applied.push(intervention.id);
280
+ }
281
+ else if (driver?.reducedMotion !== undefined) {
282
+ const preference = motionOf(declared?.[MOTION] ?? driver.reducedMotion);
283
+ await target.emulateMedia({ reducedMotion: preference });
284
+ // A declared `no-preference` is emulated and recorded among the features;
285
+ // the trick's id would claim a reduced motion the page never had.
286
+ if (preference === driver.reducedMotion)
287
+ applied.push(intervention.id);
288
+ }
289
+ else if (driver?.clock !== undefined) {
290
+ await target.addInitScript(freezeDate, driver.clock.at);
291
+ applied.push(intervention.id);
292
+ }
293
+ }
294
+ return applied;
295
+ }
296
+ function motionOf(value) {
297
+ if (value === 'reduce' || value === 'no-preference')
298
+ return value;
299
+ throw new Error(`\`${MOTION}\` is declared as ${JSON.stringify(value)}, which no browser reports; ` +
300
+ 'declare "reduce" or "no-preference"');
301
+ }
302
+ /**
303
+ * Take the pointer off the page again before a subject is read.
304
+ *
305
+ * For a host that shares the page: a test hovers a menu to open it and then
306
+ * observes, so the pointer is wherever the test's last step left it. A host
307
+ * that alone drives its page needs only {@link driveBeforeLoad}.
308
+ *
309
+ * Every reading pays the frame, hovered or not: measured in Chromium, a
310
+ * Playwright hover restyles the element while `:hover` matches nothing from
311
+ * script, so the page cannot say whether the move is needed. The pointer is
312
+ * not put back afterwards, and the move fires `mouseleave`: a menu a script
313
+ * opened on hover is closed for the test's next step.
314
+ */
315
+ export async function driveBeforeRead(recipe, target) {
316
+ const applied = [];
317
+ for (const intervention of recipe) {
318
+ if (intervention.driver?.pointer === 'outside') {
319
+ await target.mouse.move(-1, -1);
320
+ applied.push(intervention.id);
321
+ }
322
+ }
323
+ return applied;
324
+ }
325
+ /**
326
+ * The recipe as it holds on a page, given the driver tricks a host applied.
327
+ *
328
+ * A trick with no `driver` is kept. A trick with one is kept only when its id
329
+ * is in `driven`: a host that could not drive it leaves it out of the ids and
330
+ * the digest it records, rather than claiming a hold the page never had. The
331
+ * clock is the one trick whose id the driver may change, because the instant is
332
+ * the host's to choose: the recipe's clock is recorded as the one the host
333
+ * fixed.
334
+ */
335
+ export function forDriver(recipe, driven) {
336
+ const fixed = driven.map((id) => (id === freezeClock.id ? freezeClock : clockById(id))).find(Boolean);
337
+ return recipe.flatMap((intervention) => {
338
+ if (intervention.driver === undefined || driven.includes(intervention.id))
339
+ return [intervention];
340
+ return intervention.driver.clock !== undefined && fixed !== undefined ? [fixed] : [];
341
+ });
342
+ }
343
+ //# sourceMappingURL=drive.js.map