PageSourceSearch

https://lexical.dev/assets/js/3bb64832.221d9618.js

js lexical.dev collected 2026-10-02 18:24:07 UTC 8,704 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunk_lexical_website=self.webpackChunk_lexical_website||[]).push([["3404"],{88482(e,t,n){n.r(t),n.d(t,{metadata:()=>s,default:()=>h,frontMatter:()=>o,contentTitle:()=>c,toc:()=>l,assets:()=>i});var s=JSON.parse('{"id":"packages/lexical-fast-check","title":"@lexical/fast-check","description":"See API Documentation","source":"@site/docs/packages/lexical-fast-check.md","sourceDirName":"packages","slug":"/packages/lexical-fast-check","permalink":"/docs/packages/lexical-fast-check","draft":false,"unlisted":false,"editUrl":"https://github.com/facebook/lexical/tree/main/packages/lexical-fast-check/README.md","tags":[],"version":"current","frontMatter":{"custom_edit_url":"https://github.com/facebook/lexical/tree/main/packages/lexical-fast-check/README.md"},"sidebar":"docs","previous":{"title":"@lexical/extension","permalink":"/docs/packages/lexical-extension"},"next":{"title":"@lexical/file","permalink":"/docs/packages/lexical-file"}}'),a=n(41627),r=n(86958);let o={custom_edit_url:"https://github.com/facebook/lexical/tree/main/packages/lexical-fast-check/README.md"},c="@lexical/fast-check",i={},l=[{value:"Usage",id:"usage",level:2},{value:"Example: proving a clone carries your node&#39;s properties",id:"example-proving-a-clone-carries-your-nodes-properties",level:2}];function d(e){let t={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",img:"img",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.header,{children:(0,a.jsx)(t.h1,{id:"lexicalfast-check",children:(0,a.jsx)(t.code,{children:"@lexical/fast-check"})})}),"\n",(0,a.jsx)(t.p,{children:(0,a.jsx)(t.a,{href:"https://lexical.dev/docs/api/modules/lexical_fast-check",children:(0,a.jsx)(t.img,{src:"https://lexical.dev/img/see-api-documentation.svg",alt:"See API Documentation"})})}),"\n",(0,a.jsxs)(t.p,{children:["This package derives ",(0,a.jsx)(t.a,{href:"https://fast-check.dev",children:"fast-check"})," arbitraries from the\nserialization schemas Lexical nodes declare on ",(0,a.jsx)(t.code,{children:"$config"}),", so property-based tests\ngenerate exactly the serialized JSON a node's parser accepts."]}),"\n",(0,a.jsxs)(t.p,{children:["Install it alongside ",(0,a.jsx)(t.code,{children:"fast-check"}),", which is a peer dependency:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-sh",children:"npm install --save-dev @lexical/fast-check fast-check\n"})}),"\n",(0,a.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,a.jsxs)(t.p,{children:[(0,a.jsx)(t.a,{href:"https://lexical.dev/docs/api/modules/lexical_fast-check#nodearbitrary",children:(0,a.jsx)(t.code,{children:"nodeArbitrary(NodeClass)"})})," generates the node-specific properties of a\nserialized node, composing the schemas the class inherits (an element-based\nnode also gets ",(0,a.jsx)(t.code,{children:"direction"}),"/",(0,a.jsx)(t.code,{children:"format"}),"/",(0,a.jsx)(t.code,{children:"indent"}),"/\u2026 from ",(0,a.jsx)(t.code,{children:"ElementNode"}),"). Every\nproperty is generated independently as present or absent, because that is the\ndomain parsers actually face: an older document predates a property, and a\ncompact export omits one whose value equals its default."]}),"\n",(0,a.jsxs)(t.p,{children:["The values are typed as the parse shape of the class's own serialized type \u2014\n",(0,a.jsx)(t.code,{children:"LexicalParseJSON<SerializedMyNode>"})," for a ",(0,a.jsx)(t.code,{children:"MyNode"})," that declares\n",(0,a.jsx)(t.code,{children:"exportJSON(): SerializedMyNode"})," \u2014 which is what its ",(0,a.jsx)(t.code,{children:"updateFromJSON"})," accepts,\nso they can be handed to it without a cast."]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-ts",children:"import {nodeArbitrary} from '@lexical/fast-check';\nimport * as fc from 'fast-check';\n\nfc.assert(\n  fc.property(nodeArbitrary(MyNode), props => {\n    const json = {...props, type: 'my-node'};\n    // importing and re-exporting is a fixed point\n    expect(exportOf(importOf(json))).toEqual(exportOf(importOf(exportOf(importOf(json)))));\n  }),\n);\n"})}),"\n",(0,a.jsxs)(t.p,{children:[(0,a.jsx)(t.a,{href:"https://lexical.dev/docs/api/modules/lexical_fast-check#nodearbitrary",children:(0,a.jsx)(t.code,{children:"nodeArbitrary"})})," reads the class's ",(0,a.jsx)(t.code,{children:"$config"}),", so no editor or node instance is\nrequired \u2014 but resolving it injects the class's synthesized statics and compiles\nits accessor tables, so a class whose schema names an accessor it does not have\nthrows here. To inspect what a node declares without generating anything, call\n",(0,a.jsx)(t.code,{children:"getComposedSchemaFields(NodeClass)"})," from ",(0,a.jsx)(t.code,{children:"lexical"})," directly."]}),"\n",(0,a.jsx)(t.h2,{id:"example-proving-a-clone-carries-your-nodes-properties",children:"Example: proving a clone carries your node's properties"}),"\n",(0,a.jsxs)(t.p,{children:["A custom node has to copy its own properties in ",(0,a.jsx)(t.code,{children:"afterCloneFrom"}),", which Lexical\ncalls whenever ",(0,a.jsx)(t.code,{children:"getWritable()"})," clones the node. Declaring a new property and\nforgetting to copy it there is a silent bug: the field still exists on the clone\nbecause the constructor set it, so nothing throws \u2014 the node just reverts that\none property to its default the next time anything writes to it."]}),"\n",(0,a.jsxs)(t.p,{children:["This is a good fit for a generated test, because a hand-written fixture tends to\nuse default values, and a dropped property compares equal to its default. The\ntest looks like it passed precisely when the bug is invisible. ",(0,a.jsx)(t.a,{href:"https://lexical.dev/docs/api/modules/lexical_fast-check#nodearbitrary",children:(0,a.jsx)(t.code,{children:"nodeArbitrary"})}),"\ndraws from the schema's own domain instead, so the values are in-domain and\nmostly not the default:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{className:"language-ts",children:"import {nodeArbitrary} from '@lexical/fast-check';\nimport * as fc from 'fast-check';\nimport {\n  $create,\n  $createParagraphNode,\n  $getNodeByKey,\n  $getRoot,\n  createEditor,\n  TextNode,\n} from 'lexical';\n\ntest('a clone carries every property TextNode declares', () => {\n  fc.assert(\n    fc.property(nodeArbitrary(TextNode), props => {\n      const editor = createEditor({\n        nodes: [TextNode],\n        onError: e => {\n          throw e;\n        },\n      });\n      let key, before, original;\n\n      // A node is only cloned *across* updates. Within one, getWritable()\n      // hands back the same object, so a single-update test never reaches\n      // afterCloneFrom at all.\n      editor.update(() =>
1 {\n        const node = $create(TextNode);\n        // Everything but `text` is used exactly as generated. An empty\n        // TextNode is removed by normalization between the two updates, so\n        // there would be nothing left to clone; a leaf also cannot be a child\n        // of the root, hence the paragraph.\n        node.updateFromJSON({...props, text: 'x'});\n        $getRoot().clear().append($createParagraphNode().append(node));\n        [key, original, before] = [node.getKey(), node, node.exportJSON()];\n      }, {discrete: true});\n\n      editor.update(() => {\n        const writable = $getNodeByKey(key).getWritable();\n        expect(writable).not.toBe(original); // a clone really happened\n        expect(writable.exportJSON()).toEqual(before);\n      }, {discrete: true});\n    }),\n  );\n});\n"})}),"\n",(0,a.jsxs)(t.p,{children:["When it fails, fast-check shrinks to the smallest value that exposes it, which\nnames the property for you. Deleting ",(0,a.jsx)(t.code,{children:"this.__style = prevNode.__style"})," from\n",(0,a.jsx)(t.code,{children:"TextNode.afterCloneFrom"})," reports:"]}),"\n",(0,a.jsx)(t.pre,{children:(0,a.jsx)(t.code,{children:'Counterexample: [{"style":" "}]\n\n-   "style": " ",\n+   "style": "",\n'})}),"\n",(0,a.jsxs)(t.p,{children:["A single space \u2014 the smallest style that is not the default ",(0,a.jsx)(t.code,{children:"''"}),", and the only\nkind of value that can catch this."]})]})}function h(e={}){let{wrapper:t}={...(0,r.R)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(d,{...e})}):d(e)}},86958(e,t,n){n.d(t,{R:()=>o,x:()=>c});var s=n(11979);let a={},r=s.createContext(a);function o(e){let t=s.useContext(r);return s.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:o(e.components),s.createElement(r.Provider,{value:t},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.