diff --git a/.github/workflows/call-core-tests.yml b/.github/workflows/call-core-tests.yml index e6f1d5c2bd5..bb38f9e9a07 100644 --- a/.github/workflows/call-core-tests.yml +++ b/.github/workflows/call-core-tests.yml @@ -30,6 +30,7 @@ jobs: strategy: matrix: node-version: [18.18.0] + override-react-version: ['', 'beta'] env: CI: true steps: @@ -41,22 +42,10 @@ jobs: cache: npm - name: Install dependencies run: npm ci + - name: Install React ${{ matrix.override-react-version }} + if: matrix.override-react-version != '' + # This should be safe since we are caching ~/.npm and not node_modules + run: | + node ./scripts/override-react.js --version=${{ matrix.override-react-version }} + grep version node_modules/{react,react-dom}/package.json - run: npm run test-unit - - integration: - runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18.18.0] - env: - CI: true - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: npm - - name: Install dependencies - run: npm ci - - run: npm run test-integration diff --git a/.github/workflows/call-e2e-all-tests.yml b/.github/workflows/call-e2e-all-tests.yml index d1e15f11aef..8c91b5e5230 100644 --- a/.github/workflows/call-e2e-all-tests.yml +++ b/.github/workflows/call-e2e-all-tests.yml @@ -124,3 +124,20 @@ jobs: browser: ${{ matrix.browser }} editor-mode: ${{ matrix.editor-mode }} events-mode: ${{ matrix.events-mode }} + + react-beta: + strategy: + matrix: + # Currently using a single combination for every-patch e2e tests of + # react beta to reduce cost impact + editor-mode: ['rich-text'] + prod: [false] + uses: ./.github/workflows/call-e2e-test.yml + with: + os: 'macos-latest' + browser: 'chromium' + node-version: 18.18.0 + events-mode: 'modern-events' + editor-mode: ${{ matrix.editor-mode }} + prod: ${{ matrix.prod }} + override-react-version: beta diff --git a/.github/workflows/call-e2e-test.yml b/.github/workflows/call-e2e-test.yml index 48e14617237..a7ab4e4d4c7 100644 --- a/.github/workflows/call-e2e-test.yml +++ b/.github/workflows/call-e2e-test.yml @@ -9,6 +9,7 @@ on: editor-mode: {required: true, type: string} events-mode: {required: true, type: string} prod: {required: false, type: boolean} + override-react-version: {required: false, type: string} jobs: e2e-test: @@ -18,6 +19,7 @@ jobs: CI: true E2E_EDITOR_MODE: ${{ inputs.editor-mode }} E2E_EVENTS_MODE: ${{ inputs.events-mode }} + OVERRIDE_REACT_VERSION: ${{ inputs.override-react-version }} cache_playwright_path: ${{ inputs.os == 'macos-latest' && '~/Library/Caches/ms-playwright' || inputs.os == 'windows-latest' && 'C:\Users\runneradmin\AppData\Local\ms-playwright' || '~/.cache/ms-playwright' }} test_results_path: ${{ inputs.os == 'windows-latest' && '~/.npm/_logs/' || 'test-results/' }} test_script: test-e2e-${{ inputs.editor-mode == 'rich-text-with-collab' && 'collab-' || '' }}${{ inputs.prod && 'prod-' || '' }}ci-${{ inputs.browser }} @@ -35,6 +37,12 @@ jobs: sudo apt-get install xvfb - name: Install dependencies run: npm ci + - name: Install React ${{ inputs.override-react-version }} + if: inputs.override-react-version != '' + # This should be safe since we are caching ~/.npm and not node_modules + run: | + node ./scripts/override-react.js --version=${{ inputs.override-react-version }} + grep version node_modules/{react,react-dom}/package.json - name: Restore playwright from cache uses: actions/cache/restore@v4 id: playwright-cache @@ -55,6 +63,6 @@ jobs: if: failure() uses: actions/upload-artifact@v4 with: - name: Test Results ${{ inputs.os }}-${{ inputs.browser }}-${{ inputs.editor-mode }}-${{ inputs.events-mode }}-${{ inputs.prod && 'prod' || 'dev' }}-${{ inputs.node-version }} + name: Test Results ${{ inputs.os }}-${{ inputs.browser }}-${{ inputs.editor-mode }}-${{ inputs.events-mode }}-${{ inputs.prod && 'prod' || 'dev' }}-${{ inputs.node-version }}-${{ inputs.override-react-version }} path: ${{ env.test_results_path }} retention-days: 7 diff --git a/.github/workflows/call-integration-tests.yml b/.github/workflows/call-integration-tests.yml new file mode 100644 index 00000000000..9cc38f3d41a --- /dev/null +++ b/.github/workflows/call-integration-tests.yml @@ -0,0 +1,23 @@ +name: Lexical Integration Tests + +on: + workflow_call: + +jobs: + integration: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.18.0] + env: + CI: true + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + - name: Install dependencies + run: npm ci + - run: npm run test-integration diff --git a/.github/workflows/tests-extended.yml b/.github/workflows/tests-extended.yml index 9c9ff869f4c..fbb522084a8 100644 --- a/.github/workflows/tests-extended.yml +++ b/.github/workflows/tests-extended.yml @@ -2,9 +2,10 @@ name: Lexical Tests (Extended) on: pull_request: - types: [labeled, synchronize] + types: [labeled, synchronize, reopened] paths-ignore: - 'packages/lexical-website/**' + - 'packages/*/README.md' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -14,3 +15,7 @@ jobs: e2e-tests: if: github.repository_owner == 'facebook' && contains(github.event.pull_request.labels.*.name, 'extended-tests') uses: ./.github/workflows/call-e2e-all-tests.yml + + integration-tests: + if: github.repository_owner == 'facebook' && contains(github.event.pull_request.labels.*.name, 'extended-tests') + uses: ./.github/workflows/call-integration-tests.yml diff --git a/examples/react-rich-collab/package-lock.json b/examples/react-rich-collab/package-lock.json index 77c166f1d23..18f7a887890 100644 --- a/examples/react-rich-collab/package-lock.json +++ b/examples/react-rich-collab/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@lexical/react-rich-example", + "name": "@lexical/react-rich-collab-example", "version": "0.14.5", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "@lexical/react-rich-example", + "name": "@lexical/react-rich-collab-example", "version": "0.14.5", "dependencies": { "@lexical/react": "0.14.5", @@ -13,6 +13,7 @@ "lexical": "0.14.5", "react": "^18.2.0", "react-dom": "^18.2.0", + "y-webrtc": "^10.3.0", "y-websocket": "^2.0.2", "yjs": "^13.6.15" }, @@ -23,8 +24,7 @@ "concurrently": "^8.2.2", "cross-env": "^7.0.3", "typescript": "^5.2.2", - "vite": "^5.1.4", - "y-webrtc": "^10.3.0" + "vite": "^5.1.4" } }, "node_modules/@ampproject/remapping": { @@ -1341,7 +1341,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "devOptional": true, "funding": [ { "type": "github", @@ -1652,7 +1651,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -1708,8 +1706,7 @@ "node_modules/err-code": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", - "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", - "dev": true + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==" }, "node_modules/errno": { "version": "0.1.8", @@ -1805,8 +1802,7 @@ "node_modules/get-browser-rtc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz", - "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==", - "dev": true + "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==" }, "node_modules/get-caller-file": { "version": "2.0.5", @@ -1839,7 +1835,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "devOptional": true, "funding": [ { "type": "github", @@ -1864,8 +1859,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "devOptional": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", @@ -2118,8 +2112,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -2223,7 +2216,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -2243,7 +2235,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -2299,7 +2290,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "devOptional": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -2368,7 +2358,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "devOptional": true, "funding": [ { "type": "github", @@ -2435,7 +2424,6 @@ "version": "9.11.1", "resolved": "https://registry.npmjs.org/simple-peer/-/simple-peer-9.11.1.tgz", "integrity": "sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==", - "dev": true, "funding": [ { "type": "github", @@ -2464,7 +2452,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, "funding": [ { "type": "github", @@ -2503,7 +2490,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "devOptional": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -2616,8 +2602,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "devOptional": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { "version": "5.1.4", @@ -2797,7 +2782,6 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/y-webrtc/-/y-webrtc-10.3.0.tgz", "integrity": "sha512-KalJr7dCgUgyVFxoG3CQYbpS0O2qybegD0vI4bYnYHI0MOwoVbucED3RZ5f2o1a5HZb1qEssUKS0H/Upc6p1lA==", - "dev": true, "dependencies": { "lib0": "^0.2.42", "simple-peer": "^9.11.0", @@ -2824,7 +2808,6 @@ "version": "8.17.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", - "dev": true, "optional": true, "engines": { "node": ">=10.0.0" @@ -3830,8 +3813,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "devOptional": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "browserslist": { "version": "4.23.0", @@ -4022,7 +4004,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -4064,8 +4045,7 @@ "err-code": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", - "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==", - "dev": true + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==" }, "errno": { "version": "0.1.8", @@ -4135,8 +4115,7 @@ "get-browser-rtc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz", - "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==", - "dev": true + "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==" }, "get-caller-file": { "version": "2.0.5", @@ -4159,8 +4138,7 @@ "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "devOptional": true + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "immediate": { "version": "3.3.0", @@ -4171,8 +4149,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "devOptional": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "is-fullwidth-code-point": { "version": "3.0.0", @@ -4359,8 +4336,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoid": { "version": "3.3.7", @@ -4423,14 +4399,12 @@ "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, "requires": { "safe-buffer": "^5.1.0" } @@ -4470,7 +4444,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "devOptional": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -4523,8 +4496,7 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "devOptional": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "scheduler": { "version": "0.23.0", @@ -4565,7 +4537,6 @@ "version": "9.11.1", "resolved": "https://registry.npmjs.org/simple-peer/-/simple-peer-9.11.1.tgz", "integrity": "sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==", - "dev": true, "requires": { "buffer": "^6.0.3", "debug": "^4.3.2", @@ -4580,7 +4551,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -4604,7 +4574,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "devOptional": true, "requires": { "safe-buffer": "~5.2.0" } @@ -4675,8 +4644,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "devOptional": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "vite": { "version": "5.1.4", @@ -4773,7 +4741,6 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/y-webrtc/-/y-webrtc-10.3.0.tgz", "integrity": "sha512-KalJr7dCgUgyVFxoG3CQYbpS0O2qybegD0vI4bYnYHI0MOwoVbucED3RZ5f2o1a5HZb1qEssUKS0H/Upc6p1lA==", - "dev": true, "requires": { "lib0": "^0.2.42", "simple-peer": "^9.11.0", @@ -4785,7 +4752,6 @@ "version": "8.17.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", - "dev": true, "optional": true, "requires": {} } diff --git a/examples/react-rich-collab/package.json b/examples/react-rich-collab/package.json index 57c28b58a0f..96bd2a75917 100644 --- a/examples/react-rich-collab/package.json +++ b/examples/react-rich-collab/package.json @@ -1,5 +1,5 @@ { - "name": "@lexical/react-rich-example", + "name": "@lexical/react-rich-collab-example", "private": true, "version": "0.15.0", "type": "module", diff --git a/jest.config.js b/jest.config.js index b5502828557..630ceba07aa 100644 --- a/jest.config.js +++ b/jest.config.js @@ -33,6 +33,7 @@ module.exports = { ...common, displayName: 'unit', globals: { + // https://react.dev/blog/2022/03/08/react-18-upgrade-guide#configuring-your-testing-environment IS_REACT_ACT_ENVIRONMENT: true, __DEV__: true, }, diff --git a/package-lock.json b/package-lock.json index 260d3fb0377..2e9ce84f1b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -89,7 +89,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.1", "typedoc": "^0.25.12", - "typescript": "5.1.6", + "typescript": "^5.4.5", "vite": "^5.2.11" }, "engines": { @@ -29739,6 +29739,28 @@ "typedoc": ">=0.24.0" } }, + "node_modules/typedoc-plugin-rename-defaults": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-rename-defaults/-/typedoc-plugin-rename-defaults-0.7.0.tgz", + "integrity": "sha512-NudSQ1o/XLHNF9c4y7LzIZxfE9ltz09yCDklBPJpP5VMRvuBpYGIbQ0ZgmPz+EIV8vPx9Z/OyKwzp4HT2vDtfg==", + "dependencies": { + "camelcase": "^8.0.0" + }, + "peerDependencies": { + "typedoc": "0.22.x || 0.23.x || 0.24.x || 0.25.x" + } + }, + "node_modules/typedoc-plugin-rename-defaults/node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typedoc/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -29764,9 +29786,9 @@ } }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -32659,7 +32681,7 @@ "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", "lexical": "0.15.0", - "typescript": "^5.3.3", + "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" } @@ -32681,19 +32703,6 @@ "react-dom": ">=17.x" } }, - "packages/lexical-devtools/node_modules/typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "packages/lexical-dragon": { "name": "@lexical/dragon", "version": "0.15.0", @@ -32956,6 +32965,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "typedoc-plugin-markdown": "^3.17.1", + "typedoc-plugin-rename-defaults": "^0.7.0", "unist-util-visit": "^5.0.0" }, "devDependencies": { @@ -37393,18 +37403,10 @@ "lexical": "0.15.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "typescript": "^5.3.3", + "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0", "zustand": "^4.5.1" - }, - "dependencies": { - "typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", - "dev": true - } } }, "@lexical/devtools-core": { @@ -37598,6 +37600,7 @@ "react-dom": "^18.2.0", "tailwindcss": "^3.3.3", "typedoc-plugin-markdown": "^3.17.1", + "typedoc-plugin-rename-defaults": "^0.7.0", "unist-util-visit": "^5.0.0", "webpack": "^5.76.0" } @@ -54281,10 +54284,25 @@ "handlebars": "^4.7.7" } }, + "typedoc-plugin-rename-defaults": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-rename-defaults/-/typedoc-plugin-rename-defaults-0.7.0.tgz", + "integrity": "sha512-NudSQ1o/XLHNF9c4y7LzIZxfE9ltz09yCDklBPJpP5VMRvuBpYGIbQ0ZgmPz+EIV8vPx9Z/OyKwzp4HT2vDtfg==", + "requires": { + "camelcase": "^8.0.0" + }, + "dependencies": { + "camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==" + } + } + }, "typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "dev": true }, "ufo": { diff --git a/package.json b/package.json index 075e6c3445d..be73460e4c8 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ "ts-jest": "^29.1.2", "ts-node": "^10.9.1", "typedoc": "^0.25.12", - "typescript": "5.1.6", + "typescript": "^5.4.5", "vite": "^5.2.11" }, "dependencies": { diff --git a/packages/lexical-devtools/package.json b/packages/lexical-devtools/package.json index 7f40106fb86..876ef7083bd 100644 --- a/packages/lexical-devtools/package.json +++ b/packages/lexical-devtools/package.json @@ -42,7 +42,7 @@ "@types/react-dom": "^18.2.18", "@vitejs/plugin-react": "^4.2.1", "lexical": "0.15.0", - "typescript": "^5.3.3", + "typescript": "^5.4.5", "vite": "^5.2.2", "wxt": "^0.17.0" }, diff --git a/packages/lexical-devtools/tsconfig.json b/packages/lexical-devtools/tsconfig.json index 5174eae633b..cd83eb1f445 100644 --- a/packages/lexical-devtools/tsconfig.json +++ b/packages/lexical-devtools/tsconfig.json @@ -161,6 +161,7 @@ "shared/environment": ["../shared/src/environment.ts"], "shared/invariant": ["../shared/src/invariant.ts"], "shared/normalizeClassNames": ["../shared/src/normalizeClassNames.ts"], + "shared/react-test-utils": ["../shared/src/react-test-utils.ts"], "shared/simpleDiffWithCursor": ["../shared/src/simpleDiffWithCursor.ts"], "shared/useLayoutEffect": ["../shared/src/useLayoutEffect.ts"], "shared/warnOnlyOnce": ["../shared/src/warnOnlyOnce.ts"] diff --git a/packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js b/packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js index 494adecc0d8..b24e2dc3e6e 100644 --- a/packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js +++ b/packages/lexical-eslint-plugin/src/rules/rules-of-lexical.js @@ -285,7 +285,9 @@ module.exports.rulesOfLexical = { const pushIgnoredNode = (/** @type {Node} */ node) => ignoreSet.add(node); const popIgnoredNode = (/** @type {Node} */ node) => ignoreSet.delete(node); const pushFunction = (/** @type {Node} */ node) => { - const name = getFunctionNameIdentifier(getLexicalFunctionName(node)); + const name = getFunctionNameIdentifier( + /** @type {Node | undefined} */ (getLexicalFunctionName(node)), + ); funStack.push({name, node}); if ( matchers.isDollarFunction(name) || diff --git a/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx b/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx index a27095d3201..3a495a42e48 100644 --- a/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx +++ b/packages/lexical-history/src/__tests__/unit/LexicalHistory.test.tsx @@ -35,7 +35,7 @@ import { import {createTestEditor, TestComposer} from 'lexical/src/__tests__/utils'; import React from 'react'; import {createRoot, Root} from 'react-dom/client'; -import * as ReactTestUtils from 'react-dom/test-utils'; +import * as ReactTestUtils from 'shared/react-test-utils'; describe('LexicalHistory tests', () => { let container: HTMLDivElement | null = null; diff --git a/packages/lexical-playground/vite.config.ts b/packages/lexical-playground/vite.config.ts index bbfcd2cc45a..d7d0d3b5c48 100644 --- a/packages/lexical-playground/vite.config.ts +++ b/packages/lexical-playground/vite.config.ts @@ -73,7 +73,11 @@ export default defineConfig(({command}) => { }), react(), viteCopyEsm(), - commonjs(), + commonjs({ + // This is required for React 19 (at least 19.0.0-beta-26f2496093-20240514) + // because @rollup/plugin-commonjs does not analyze it correctly + strictRequires: [/\/node_modules\/(react-dom|react)\/[^/]\.js$/], + }), ], resolve: { alias: moduleResolution(command === 'serve' ? 'source' : 'development'), diff --git a/packages/lexical-playground/vite.prod.config.ts b/packages/lexical-playground/vite.prod.config.ts index ad4b3499cec..083f4697032 100644 --- a/packages/lexical-playground/vite.prod.config.ts +++ b/packages/lexical-playground/vite.prod.config.ts @@ -66,7 +66,11 @@ export default defineConfig({ }), react(), viteCopyEsm(), - commonjs(), + commonjs({ + // This is required for React 19 (at least 19.0.0-beta-26f2496093-20240514) + // because @rollup/plugin-commonjs does not analyze it correctly + strictRequires: [/\/node_modules\/(react-dom|react)\/[^/]\.js$/], + }), ], resolve: { alias: moduleResolution('production'), diff --git a/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx b/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx index 2a79c9bc636..0b8d3d2b38c 100644 --- a/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx +++ b/packages/lexical-react/src/__tests__/unit/LexicalComposer.test.tsx @@ -7,13 +7,19 @@ */ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext'; +import { + $createParagraphNode, + $createTextNode, + $getRoot, + LexicalEditor, +} from 'lexical'; import * as React from 'react'; import {createRoot, Root} from 'react-dom/client'; -import * as ReactTestUtils from 'react-dom/test-utils'; +import * as ReactTestUtils from 'shared/react-test-utils'; import {LexicalComposer} from '../../LexicalComposer'; -describe('LexicalNodeHelpers tests', () => { +describe('LexicalComposer tests', () => { let container: HTMLDivElement | null = null; let reactRoot: Root; @@ -59,4 +65,65 @@ describe('LexicalNodeHelpers tests', () => { reactRoot.render(); }); }); + + describe('LexicalComposerContext editor identity', () => { + ( + [ + {name: 'StrictMode', size: 2}, + {name: 'Fragment', size: 1}, + ] as const + ).forEach(({name, size}) => { + const Wrapper = React[name]; + const editors = new Set(); + const pluginEditors = new Set(); + function Plugin() { + pluginEditors.add(useLexicalComposerContext()[0]); + return null; + } + function App() { + return ( + { + const p = $createParagraphNode(); + p.append($createTextNode('initial state')); + $getRoot().append(p); + }); + }, + namespace: '', + nodes: [], + onError: () => { + throw Error(); + }, + }}> + + + ); + } + it(`renders ${size} editors under ${name}`, async () => { + await ReactTestUtils.act(async () => { + reactRoot.render( + + + , + ); + }); + // 2 editors may be created since useMemo is still called twice, + // but only one result is used! + expect(editors.size).toBe(size); + [...editors].forEach((editor, i) => { + // This confirms that editorState() was only called once per editor, + // otherwise you could see 'initial stateinitial state'. + expect([ + i, + editor.getEditorState().read(() => $getRoot().getTextContent()), + ]).toEqual([i, 'initial state']); + }); + // Only one context is created in both cases though! + expect(pluginEditors.size).toBe(1); + }); + }); + }); }); diff --git a/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx b/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx index 9a36cc4fdbe..62cc7bd850a 100644 --- a/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx +++ b/packages/lexical-react/src/__tests__/unit/PlainRichTextPlugin.test.tsx @@ -26,7 +26,7 @@ import { } from 'lexical'; import * as React from 'react'; import {createRoot, Root} from 'react-dom/client'; -import * as ReactTestUtils from 'react-dom/test-utils'; +import * as ReactTestUtils from 'shared/react-test-utils'; import {LexicalComposer} from '../../LexicalComposer'; import {ContentEditable} from '../../LexicalContentEditable'; diff --git a/packages/lexical-react/src/__tests__/unit/React19.test.tsx b/packages/lexical-react/src/__tests__/unit/React19.test.tsx new file mode 100644 index 00000000000..a59eaeb181b --- /dev/null +++ b/packages/lexical-react/src/__tests__/unit/React19.test.tsx @@ -0,0 +1,51 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import * as React from 'react'; +import {createRoot, Root} from 'react-dom/client'; +import * as ReactTestUtils from 'shared/react-test-utils'; + +const IS_REACT_19 = parseInt(React.version.split('.')[0], 10) >= 19; +const OVERRIDE_REACT_VERSION = process.env.OVERRIDE_REACT_VERSION ?? ''; + +describe(`React expectations (${React.version}) OVERRIDE_REACT_VERSION=${OVERRIDE_REACT_VERSION}`, () => { + let container: HTMLDivElement; + let reactRoot: Root; + beforeEach(() => { + container = document.createElement('div'); + reactRoot = createRoot(container); + document.body.appendChild(container); + }); + // This checks our assumption that we are testing against the correct version of React + // The inverse is not checked so the test doesn't fail when our dependencies + // are upgraded. + if (OVERRIDE_REACT_VERSION) { + test(`Expecting React >= 19`, () => { + expect(IS_REACT_19).toBe(true); + }); + } + const cacheExpect = IS_REACT_19 ? 'cached' : 'not cached'; + test(`StrictMode useMemo is ${cacheExpect}`, () => { + const memoFun = jest + .fn() + .mockReturnValueOnce('cached') + .mockReturnValue('not cached'); + function MemoComponent() { + return React.useMemo(memoFun, []); + } + ReactTestUtils.act(() => { + reactRoot.render( + + + , + ); + }); + expect(container.textContent).toBe(cacheExpect); + expect(memoFun).toBeCalledTimes(2); + }); +}); diff --git a/packages/lexical-react/src/__tests__/unit/useLexicalIsTextContentEmpty.test.tsx b/packages/lexical-react/src/__tests__/unit/useLexicalIsTextContentEmpty.test.tsx index 652946f168b..9d1c30f7bdf 100644 --- a/packages/lexical-react/src/__tests__/unit/useLexicalIsTextContentEmpty.test.tsx +++ b/packages/lexical-react/src/__tests__/unit/useLexicalIsTextContentEmpty.test.tsx @@ -17,7 +17,7 @@ import { import * as React from 'react'; import {createRef} from 'react'; import {createRoot, Root} from 'react-dom/client'; -import * as ReactTestUtils from 'react-dom/test-utils'; +import * as ReactTestUtils from 'shared/react-test-utils'; import {useLexicalIsTextContentEmpty} from '../../useLexicalIsTextContentEmpty'; diff --git a/packages/lexical-react/src/__tests__/unit/utils.tsx b/packages/lexical-react/src/__tests__/unit/utils.tsx index 46824a9a6ad..3f1433f3534 100644 --- a/packages/lexical-react/src/__tests__/unit/utils.tsx +++ b/packages/lexical-react/src/__tests__/unit/utils.tsx @@ -11,7 +11,7 @@ import {LexicalEditor} from 'lexical'; import * as React from 'react'; import {Container} from 'react-dom'; import {createRoot, Root} from 'react-dom/client'; -import * as ReactTestUtils from 'react-dom/test-utils'; +import * as ReactTestUtils from 'shared/react-test-utils'; import * as Y from 'yjs'; import {useCollaborationContext} from '../../LexicalCollaborationContext'; diff --git a/packages/lexical-react/src/shared/useCharacterLimit.ts b/packages/lexical-react/src/shared/useCharacterLimit.ts index 6d0c0782da8..a365bea22b9 100644 --- a/packages/lexical-react/src/shared/useCharacterLimit.ts +++ b/packages/lexical-react/src/shared/useCharacterLimit.ts @@ -101,7 +101,6 @@ function findOffset( maxCharacters: number, strlen: (input: string) => number, ): number { - // @ts-ignore This is due to be added in a later version of TS const Segmenter = Intl.Segmenter; let offsetUtf16 = 0; let offset = 0; diff --git a/packages/lexical-react/src/useLexicalEditable.ts b/packages/lexical-react/src/useLexicalEditable.ts index f57b67ccc5e..a561f156d46 100644 --- a/packages/lexical-react/src/useLexicalEditable.ts +++ b/packages/lexical-react/src/useLexicalEditable.ts @@ -20,6 +20,14 @@ function subscription(editor: LexicalEditor): LexicalSubscription { }; } +/** + * Get the current value for {@link LexicalEditor.isEditable} + * using {@link useLexicalSubscription}. + * You should prefer this over manually observing the value with + * {@link LexicalEditor.registerEditableListener}, + * which is a bit tricky to do correctly, particularly when using + * React StrictMode (the default for development) or concurrency. + */ export function useLexicalEditable(): boolean { return useLexicalSubscription(subscription); } diff --git a/packages/lexical-react/src/useLexicalSubscription.tsx b/packages/lexical-react/src/useLexicalSubscription.tsx index bb59b4768bf..c8230c40033 100644 --- a/packages/lexical-react/src/useLexicalSubscription.tsx +++ b/packages/lexical-react/src/useLexicalSubscription.tsx @@ -19,6 +19,7 @@ export type LexicalSubscription = { /** * Shortcut to Lexical subscriptions when values are used for render. + * @param subscription - The function to create the {@link LexicalSubscription}. This function's identity must be stable (e.g. defined at module scope or with useCallback). */ export function useLexicalSubscription( subscription: (editor: LexicalEditor) => LexicalSubscription, diff --git a/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts b/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts index a58f759e0ea..39b10047c4a 100644 --- a/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts +++ b/packages/lexical-rich-text/src/__tests__/unit/LexicalHeadingNode.test.ts @@ -44,8 +44,7 @@ describe('LexicalHeadingNode tests', () => { expect(headingNode.getTag()).toBe('h1'); expect(headingNode.getTextContent()).toBe(''); }); - // @ts-ignore - expect(() => new HeadingNode()).toThrow(); + expect(() => new HeadingNode('h1')).toThrow(); }); test('HeadingNode.createDOM()', async () => { diff --git a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx index 9c6d435e587..2a9930a4276 100644 --- a/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx +++ b/packages/lexical-selection/src/__tests__/unit/LexicalSelection.test.tsx @@ -36,6 +36,7 @@ import { LexicalNode, ParagraphNode, PointType, + type RangeSelection, TextNode, } from 'lexical'; import { @@ -47,8 +48,8 @@ import { invariant, TestComposer, } from 'lexical/src/__tests__/utils'; -import {createRoot} from 'react-dom/client'; -import * as ReactTestUtils from 'react-dom/test-utils'; +import {createRoot, Root} from 'react-dom/client'; +import * as ReactTestUtils from 'shared/react-test-utils'; import { $setAnchorPoint, @@ -113,24 +114,27 @@ Range.prototype.getBoundingClientRect = function (): DOMRect { }; describe('LexicalSelection tests', () => { - let container: HTMLElement | null = null; + let container: HTMLElement; + let reactRoot: Root; + let editor: LexicalEditor | null = null; beforeEach(async () => { container = document.createElement('div'); document.body.appendChild(container); - + reactRoot = createRoot(container); await init(); }); - afterEach(() => { - if (container) { - document.body.removeChild(container); - } - container = null; + afterEach(async () => { + // Ensure we are clearing out any React state and running effects with + // act + await ReactTestUtils.act(async () => { + reactRoot.unmount(); + await Promise.resolve().then(); + }); + document.body.removeChild(container); }); - let editor: LexicalEditor | null = null; - async function init() { function TestBase() { function TestPlugin() { @@ -187,10 +191,10 @@ describe('LexicalSelection tests', () => { ); } - ReactTestUtils.act(() => { - createRoot(container!).render(); + await ReactTestUtils.act(async () => { + reactRoot.render(); + await Promise.resolve().then(); }); - editor!.getRootElement()!.focus(); await Promise.resolve().then(); @@ -208,8 +212,6 @@ describe('LexicalSelection tests', () => { await ReactTestUtils.act(async () => { await editor!.update(fn); }); - - return Promise.resolve().then(); } test('Expect initial output to be a block with no text.', () => { @@ -1132,7 +1134,7 @@ describe('LexicalSelection tests', () => { await applySelectionInputs(testUnit.inputs, update, editor!); // Validate HTML matches - expect(container!.innerHTML).toBe(testUnit.expectedHTML); + expect(container.innerHTML).toBe(testUnit.expectedHTML); // Validate selection matches const rootElement = editor!.getRootElement()!; @@ -2653,7 +2655,6 @@ describe('LexicalSelection tests', () => { offset: text.__text.length, type: 'text', }); - // @ts-ignore const selection = $getSelection() as RangeSelection; const columnChildrenPrev = column.getChildren(); @@ -2721,7 +2722,6 @@ describe('LexicalSelection tests', () => { offset: 0, type: 'element', }); - // @ts-ignore const selection = $getSelection() as RangeSelection; $setBlocksType(selection, () => { diff --git a/packages/lexical-selection/src/__tests__/utils/index.ts b/packages/lexical-selection/src/__tests__/utils/index.ts index b8317d56b3e..84c82edecfa 100644 --- a/packages/lexical-selection/src/__tests__/utils/index.ts +++ b/packages/lexical-selection/src/__tests__/utils/index.ts @@ -32,7 +32,6 @@ type Segment = { segment: string; }; -// @ts-ignore if (!Selection.prototype.modify) { const wordBreakPolyfillRegex = /[\s.,\\/#!$%^&*;:{}=\-`~()\uD800-\uDBFF\uDC00-\uDFFF\u3000-\u303F]/u; @@ -87,7 +86,6 @@ if (!Selection.prototype.modify) { return segments; }; - // @ts-ignore Selection.prototype.modify = function (alter, direction, granularity) { // This is not a thorough implementation, it was more to get tests working // given the refactor to use this selection method. diff --git a/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx b/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx index 7112c101222..a3dab04aef0 100644 --- a/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx +++ b/packages/lexical-table/src/__tests__/unit/LexicalTableSelection.test.tsx @@ -21,7 +21,7 @@ import { import {createTestEditor} from 'lexical/src/__tests__/utils'; import {createRef, useEffect, useMemo} from 'react'; import {createRoot, Root} from 'react-dom/client'; -import * as ReactTestUtils from 'react-dom/test-utils'; +import * as ReactTestUtils from 'shared/react-test-utils'; describe('table selection', () => { let originalText: TextNode; diff --git a/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx b/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx index de2bfcb7950..be8e06d3b2c 100644 --- a/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx +++ b/packages/lexical-utils/src/__tests__/unit/LexicalEventHelpers.test.tsx @@ -25,7 +25,7 @@ import {TableCellNode, TableNode, TableRowNode} from '@lexical/table'; import {LexicalEditor} from 'lexical'; import {initializeClipboard, TestComposer} from 'lexical/src/__tests__/utils'; import {createRoot} from 'react-dom/client'; -import * as ReactTestUtils from 'react-dom/test-utils'; +import * as ReactTestUtils from 'shared/react-test-utils'; jest.mock('shared/environment', () => { const originalModule = jest.requireActual('shared/environment'); diff --git a/packages/lexical-website/docs/concepts/editor-state.md b/packages/lexical-website/docs/concepts/editor-state.md index 915eaf9783e..8ddd92bf098 100644 --- a/packages/lexical-website/docs/concepts/editor-state.md +++ b/packages/lexical-website/docs/concepts/editor-state.md @@ -87,17 +87,19 @@ const onSubmit = () => { } ``` -For React it could be something following: +For React it could be something like the following: ```jsx const initialEditorState = await loadContent(); -const editorStateRef = useRef(); +const editorStateRef = useRef(undefined); - editorStateRef.current = editorState} /> + { + editorStateRef.current = editorState; + }} />