diff --git a/package.json b/package.json index b0d160445f..a662bb9ab5 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "devDependencies": { "@payloadcms/eslint-config": "workspace:*", - "@playwright/test": "1.38.1", + "@playwright/test": "1.39.0", "@swc/cli": "^0.1.62", "@swc/jest": "0.2.29", "@swc/register": "0.1.10", @@ -45,7 +45,7 @@ "@types/conventional-changelog-core": "^4.2.5", "@types/conventional-changelog-preset-loader": "^2.3.4", "@types/fs-extra": "^11.0.2", - "@types/jest": "29.5.4", + "@types/jest": "29.5.7", "@types/minimist": "1.2.2", "@types/node": "20.5.7", "@types/prompts": "^2.4.5", @@ -64,6 +64,7 @@ "copyfiles": "2.4.1", "cross-env": "7.0.3", "dotenv": "8.6.0", + "drizzle-orm": "0.28.5", "express": "4.18.2", "form-data": "3.0.1", "fs-extra": "10.1.0", @@ -73,8 +74,8 @@ "graphql-request": "6.1.0", "husky": "^8.0.3", "isomorphic-fetch": "3.0.0", - "jest": "29.6.4", - "jest-environment-jsdom": "29.6.4", + "jest": "29.7.0", + "jest-environment-jsdom": "29.7.0", "jwt-decode": "3.1.2", "lint-staged": "^14.0.1", "minimist": "1.2.8", diff --git a/packages/richtext-lexical/src/field/features/debug/TestRecorder/index.ts b/packages/richtext-lexical/src/field/features/debug/TestRecorder/index.ts new file mode 100644 index 0000000000..ed4435a896 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/debug/TestRecorder/index.ts @@ -0,0 +1,20 @@ +import type { FeatureProvider } from '../../types' + +import { TestRecorderPlugin } from './plugin' + +export const TestRecorderFeature = (): FeatureProvider => { + return { + feature: () => { + return { + plugins: [ + { + Component: TestRecorderPlugin, + position: 'bottom', + }, + ], + props: null, + } + }, + key: 'debug-testrecorder', + } +} diff --git a/packages/richtext-lexical/src/field/features/debug/TestRecorder/plugin/index.scss b/packages/richtext-lexical/src/field/features/debug/TestRecorder/plugin/index.scss new file mode 100644 index 0000000000..d6f90cdc38 --- /dev/null +++ b/packages/richtext-lexical/src/field/features/debug/TestRecorder/plugin/index.scss @@ -0,0 +1,51 @@ +.test-recorder-output { + margin: 20px auto 20px auto; + width: 100%; +} +.test-recorder-toolbar { + display: flex; +} + +.test-recorder-button { + position: relative; + display: block; + font-size: 10px; + padding: 6px 6px; + border-radius: 4px; + border: none; + cursor: pointer; + outline: none; + box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.4); + background-color: #222; + color: white; + transition: box-shadow 50ms ease-out; +} + +.test-recorder-button:active { + box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.4); +} + +.test-recorder-button + .test-recorder-button { + margin-left: 4px; +} + +.test-recorder-button::after { + content: ''; + position: absolute; + top: 8px; + right: 8px; + bottom: 8px; + left: 8px; + display: block; + background-size: contain; + filter: invert(1); +} +#test-recorder-button { + position: relative; +} + +#test-recorder-button-snapshot { + margin-right: auto; +} + + diff --git a/packages/richtext-lexical/src/field/features/debug/TestRecorder/plugin/index.tsx b/packages/richtext-lexical/src/field/features/debug/TestRecorder/plugin/index.tsx new file mode 100644 index 0000000000..332fc2aacf --- /dev/null +++ b/packages/richtext-lexical/src/field/features/debug/TestRecorder/plugin/index.tsx @@ -0,0 +1,452 @@ +import type { GridSelection, LexicalEditor, NodeSelection, RangeSelection } from 'lexical' + +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical' +import * as React from 'react' +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' + +import { IS_APPLE } from '../../../../lexical/utils/environment' +import './index.scss' + +const copy = (text: null | string) => { + const textArea = document.createElement('textarea') + textArea.value = text || '' + textArea.style.position = 'absolute' + textArea.style.opacity = '0' + document.body?.appendChild(textArea) + textArea.focus() + textArea.select() + try { + const result = document.execCommand('copy') + // eslint-disable-next-line no-console + console.log(result) + } catch (error) { + console.error(error) + } + document.body?.removeChild(textArea) +} + +const download = (filename: string, text: null | string) => { + const a = document.createElement('a') + a.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text || '')) + a.setAttribute('download', filename) + a.style.display = 'none' + document.body?.appendChild(a) + a.click() + document.body?.removeChild(a) +} + +const formatStep = (step: Step) => { + const formatOneStep = (name: string, value: Step['value']) => { + switch (name) { + case 'click': { + return ` await page.mouse.click(${value.x}, ${value.y});` + } + case 'press': { + return ` await page.keyboard.press('${value}');` + } + case 'keydown': { + return ` await page.keyboard.keydown('${value}');` + } + case 'keyup': { + return ` await page.keyboard.keyup('${value}');` + } + case 'type': { + return ` await page.keyboard.type('${value}');` + } + case 'selectAll': { + return ` await selectAll(page);` + } + case 'snapshot': { + return ` await assertHTMLSnapshot(page); + await assertSelection(page, { + anchorPath: [${value.anchorPath.toString()}], + anchorOffset: ${value.anchorOffset}, + focusPath: [${value.focusPath.toString()}], + focusOffset: ${value.focusOffset}, + }); +` + } + default: + return `` + } + } + const formattedStep = formatOneStep(step.name, step.value) + switch (step.count) { + case 1: + return formattedStep + case 2: + return [formattedStep, formattedStep].join(`\n`) + default: + return ` await repeat(${step.count}, async () => { + ${formattedStep} + );` + } +} + +export function isSelectAll(event: KeyboardEvent): boolean { + return event.keyCode === 65 && (IS_APPLE ? event.metaKey : event.ctrlKey) +} + +// stolen from LexicalSelection-test +function sanitizeSelection(selection: Selection) { + const { anchorNode, focusNode } = selection + let { anchorOffset, focusOffset } = selection + if (anchorOffset !== 0) { + anchorOffset-- + } + if (focusOffset !== 0) { + focusOffset-- + } + return { anchorNode, anchorOffset, focusNode, focusOffset } +} + +function getPathFromNodeToEditor(node: Node, rootElement: HTMLElement | null) { + let currentNode: Node | null | undefined = node + const path = [] + while (currentNode !== rootElement) { + if (currentNode !== null && currentNode !== undefined) { + path.unshift( + Array.from(currentNode?.parentNode?.childNodes ?? []).indexOf(currentNode as ChildNode), + ) + } + currentNode = currentNode?.parentNode + } + return path +} + +const keyPresses = new Set([ + 'Enter', + 'Backspace', + 'Delete', + 'Escape', + 'ArrowLeft', + 'ArrowRight', + 'ArrowUp', + 'ArrowDown', +]) + +type Step = { + count: number + name: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any +} + +type Steps = Step[] + +function useTestRecorder(editor: LexicalEditor): [JSX.Element, JSX.Element | null] { + const [steps, setSteps] = useState([]) + const [isRecording, setIsRecording] = useState(false) + const [, setCurrentInnerHTML] = useState('') + const [templatedTest, setTemplatedTest] = useState('') + const previousSelectionRef = useRef(null) + const skipNextSelectionChangeRef = useRef(false) + const preRef = useRef(null) + + const getCurrentEditor = useCallback(() => { + return editor + }, [editor]) + + const generateTestContent = useCallback(() => { + const rootElement = editor.getRootElement() + const browserSelection = window.getSelection() + + if ( + rootElement == null || + browserSelection == null || + browserSelection.anchorNode == null || + browserSelection.focusNode == null || + !rootElement.contains(browserSelection.anchorNode) || + !rootElement.contains(browserSelection.focusNode) + ) { + return null + } + + return ` +import { + initializeE2E, + assertHTMLSnapshot, + assertSelection, + repeat, +} from '../utils'; +import {selectAll} from '../keyboardShortcuts'; +import { RangeSelection } from 'lexical'; +import { NodeSelection } from 'lexical'; + +describe('Test case', () => { + initializeE2E((e2e) => { + it('Should pass this test', async () => { + const {page} = e2e; + + await page.focus('div[contenteditable="true"]'); +${steps.map(formatStep).join(`\n`)} + }); +}); + ` + }, [editor, steps]) + + // just a wrapper around inserting new actions so that we can + // coalesce some actions like insertText/moveNativeSelection + const pushStep = useCallback( + (name: string, value: Step['value']) => { + setSteps((currentSteps) => { + // trying to group steps + const currentIndex = steps.length - 1 + const lastStep = steps[currentIndex] + if (lastStep) { + if (lastStep.name === name) { + if (name === 'type') { + // for typing events we just append the text + return [ + ...steps.slice(0, currentIndex), + { ...lastStep, value: lastStep.value + value }, + ] + } else { + // for other events we bump the counter if their values are the same + if (lastStep.value === value) { + return [...steps.slice(0, currentIndex), { ...lastStep, count: lastStep.count + 1 }] + } + } + } + } + // could not group, just append a new one + return [...currentSteps, { name, count: 1, value }] + }) + }, + [steps, setSteps], + ) + + useLayoutEffect(() => { + const onKeyDown = (event: KeyboardEvent) => { + if (!isRecording) { + return + } + const key = event.key + if (isSelectAll(event)) { + pushStep('selectAll', '') + } else if (keyPresses.has(key)) { + pushStep('press', event.key) + } else if ([...key].length > 1) { + pushStep('keydown', event.key) + } else { + pushStep('type', event.key) + } + } + + const onKeyUp = (event: KeyboardEvent) => { + if (!isRecording) { + return + } + const key = event.key + if (!keyPresses.has(key) && [...key].length > 1) { + pushStep('keyup', event.key) + } + } + + return editor.registerRootListener( + (rootElement: HTMLElement | null, prevRootElement: HTMLElement | null) => { + if (prevRootElement !== null) { + prevRootElement.removeEventListener('keydown', onKeyDown) + prevRootElement.removeEventListener('keyup', onKeyUp) + } + if (rootElement !== null) { + rootElement.addEventListener('keydown', onKeyDown) + rootElement.addEventListener('keyup', onKeyUp) + } + }, + ) + }, [editor, isRecording, pushStep]) + + useLayoutEffect(() => { + if (preRef.current) { + preRef.current.scrollTo(0, preRef.current.scrollHeight) + } + }, [generateTestContent]) + + useEffect(() => { + if (steps) { + const testContent = generateTestContent() + if (testContent !== null) { + setTemplatedTest(testContent) + } + if (preRef.current) { + preRef.current.scrollTo(0, preRef.current.scrollHeight) + } + } + }, [generateTestContent, steps]) + + useEffect(() => { + const removeUpdateListener = editor.registerUpdateListener( + ({ dirtyElements, dirtyLeaves, editorState }) => { + if (!isRecording) { + return + } + const currentSelection = editorState._selection + const previousSelection = previousSelectionRef.current + const skipNextSelectionChange = skipNextSelectionChangeRef.current + if (previousSelection !== currentSelection) { + if (dirtyLeaves.size === 0 && dirtyElements.size === 0 && !skipNextSelectionChange) { + const browserSelection = window.getSelection() + if ( + browserSelection && + (browserSelection.anchorNode == null || browserSelection.focusNode == null) + ) { + return + } + } + previousSelectionRef.current = currentSelection + } + skipNextSelectionChangeRef.current = false + const testContent = generateTestContent() + if (testContent !== null) { + setTemplatedTest(testContent) + } + }, + ) + return removeUpdateListener + }, [editor, generateTestContent, isRecording, pushStep]) + + // save innerHTML + useEffect(() => { + if (!isRecording) { + return + } + const removeUpdateListener = editor.registerUpdateListener(() => { + const rootElement = editor.getRootElement() + if (rootElement !== null) { + setCurrentInnerHTML(rootElement?.innerHTML) + } + }) + return removeUpdateListener + }, [editor, isRecording]) + + // clear editor and start recording + const toggleEditorSelection = useCallback( + (currentEditor: LexicalEditor) => { + if (!isRecording) { + currentEditor.update(() => { + const root = $getRoot() + root.clear() + const text = $createTextNode() + root.append($createParagraphNode().append(text)) + text.select() + }) + setSteps([]) + } + setIsRecording((currentIsRecording) => !currentIsRecording) + }, + [isRecording], + ) + + const onSnapshotClick = useCallback(() => { + if (!isRecording) { + return + } + const browserSelection = window.getSelection() + if ( + browserSelection === null || + browserSelection.anchorNode == null || + browserSelection.focusNode == null + ) { + return + } + const { anchorNode, anchorOffset, focusNode, focusOffset } = sanitizeSelection(browserSelection) + const rootElement = getCurrentEditor().getRootElement() + let anchorPath + if (anchorNode !== null) { + anchorPath = getPathFromNodeToEditor(anchorNode, rootElement) + } + let focusPath + if (focusNode !== null) { + focusPath = getPathFromNodeToEditor(focusNode, rootElement) + } + pushStep('snapshot', { + anchorNode, + anchorOffset, + anchorPath, + focusNode, + focusOffset, + focusPath, + }) + }, [pushStep, isRecording, getCurrentEditor]) + + const onCopyClick = useCallback(() => { + copy(generateTestContent()) + }, [generateTestContent]) + + const onDownloadClick = useCallback(() => { + download('test.js', generateTestContent()) + }, [generateTestContent]) + + const button = ( + + ) + const output = isRecording ? ( +
+
+ + + +
+
+        {templatedTest}
+      
+
+ ) : null + + return [button, output] +} +export const TestRecorderPlugin: React.FC = () => { + const [editor] = useLexicalComposerContext() + const [testRecorderButton, testRecorderOutput] = useTestRecorder(editor) + + return ( + +

HI

+ {testRecorderButton} + {testRecorderOutput} +

DONE

+
+ ) +} diff --git a/packages/richtext-lexical/src/field/features/debug/TreeView/index.ts b/packages/richtext-lexical/src/field/features/debug/TreeView/index.ts index 1bf01a04e0..188157b816 100644 --- a/packages/richtext-lexical/src/field/features/debug/TreeView/index.ts +++ b/packages/richtext-lexical/src/field/features/debug/TreeView/index.ts @@ -10,7 +10,7 @@ export const TreeviewFeature = (): FeatureProvider => { plugins: [ { Component: TreeViewPlugin, - position: 'normal', + position: 'bottom', }, ], props: null, diff --git a/packages/richtext-lexical/src/field/features/types.ts b/packages/richtext-lexical/src/field/features/types.ts index f8eceb7f69..52aeb90f7e 100644 --- a/packages/richtext-lexical/src/field/features/types.ts +++ b/packages/richtext-lexical/src/field/features/types.ts @@ -89,6 +89,11 @@ export type Feature = { Component: React.FC position: 'normal' // Determines at which position the Component will be added. } + | { + // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality + Component: React.FC<{ anchorElem: HTMLElement }> + position: 'bottom' // Determines at which position the Component will be added. + } | { // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality Component: React.FC<{ anchorElem: HTMLElement }> @@ -191,6 +196,12 @@ export type SanitizedFeatures = Required< key: string position: 'floatingAnchorElem' // Determines at which position the Component will be added. } + | { + // plugins are anything which is not directly part of the editor. Like, creating a command which creates a node, or opens a modal, or some other more "outside" functionality + Component: React.FC + key: string + position: 'bottom' // Determines at which position the Component will be added. + } > /** The node types mapped to their populationPromises */ populationPromises: Map> diff --git a/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx b/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx index 825daec803..37756ab759 100644 --- a/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx +++ b/packages/richtext-lexical/src/field/lexical/LexicalEditor.tsx @@ -111,6 +111,11 @@ export const LexicalEditor: React.FC } })} + {editorConfig.features.plugins.map((plugin) => { + if (plugin.position === 'bottom') { + return + } + })} ) } diff --git a/packages/richtext-lexical/src/field/lexical/utils/environment.ts b/packages/richtext-lexical/src/field/lexical/utils/environment.ts new file mode 100644 index 0000000000..16fca7cbe0 --- /dev/null +++ b/packages/richtext-lexical/src/field/lexical/utils/environment.ts @@ -0,0 +1,44 @@ +/** + * 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 { CAN_USE_DOM } from './canUseDOM' + +declare global { + interface Document { + documentMode?: unknown + } + + interface Window { + MSStream?: unknown + } +} + +const documentMode = CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null + +export const IS_APPLE: boolean = CAN_USE_DOM && /Mac|iPod|iPhone|iPad/.test(navigator.platform) + +export const IS_FIREFOX: boolean = + CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent) + +export const CAN_USE_BEFORE_INPUT: boolean = + CAN_USE_DOM && 'InputEvent' in window && !documentMode + ? 'getTargetRanges' in new window.InputEvent('input') + : false + +export const IS_SAFARI: boolean = CAN_USE_DOM && /Version\/[\d.].*Safari/.test(navigator.userAgent) + +export const IS_IOS: boolean = + CAN_USE_DOM && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream + +export const IS_ANDROID: boolean = CAN_USE_DOM && /Android/.test(navigator.userAgent) + +export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform) +export const IS_CHROME: boolean = CAN_USE_DOM && /^(?=.*Chrome).*/i.test(navigator.userAgent) +// export const canUseTextInputEvent: boolean = CAN_USE_DOM && 'TextEvent' in window && !documentMode; + +export const IS_APPLE_WEBKIT = + CAN_USE_DOM && /AppleWebKit\/[\d.]+/.test(navigator.userAgent) && !IS_CHROME diff --git a/packages/richtext-lexical/src/index.ts b/packages/richtext-lexical/src/index.ts index 9d9d911fde..64d0f4b0de 100644 --- a/packages/richtext-lexical/src/index.ts +++ b/packages/richtext-lexical/src/index.ts @@ -185,6 +185,7 @@ export type { HTMLConverter } from './field/features/converters/html/converter/t export { consolidateHTMLConverters } from './field/features/converters/html/field' export { lexicalHTML } from './field/features/converters/html/field' +export { TestRecorderFeature } from './field/features/debug/TestRecorder' export { TreeviewFeature } from './field/features/debug/TreeView' export { BoldTextFeature } from './field/features/format/Bold' diff --git a/playwright.config.ts b/playwright.config.ts index 2aae8af583..9c92b2f8c9 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -10,6 +10,9 @@ const config: PlaywrightTestConfig = { trace: 'retain-on-failure', video: 'retain-on-failure', }, - workers: 999, + expect: { + timeout: 10000, + }, + workers: 16, } export default config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4391e76d91..6d24b694d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,8 +22,8 @@ importers: specifier: workspace:* version: link:packages/eslint-config-payload '@playwright/test': - specifier: 1.38.1 - version: 1.38.1 + specifier: 1.39.0 + version: 1.39.0 '@swc/cli': specifier: ^0.1.62 version: 0.1.62(@swc/core@1.3.76) @@ -55,8 +55,8 @@ importers: specifier: ^11.0.2 version: 11.0.2 '@types/jest': - specifier: 29.5.4 - version: 29.5.4 + specifier: 29.5.7 + version: 29.5.7 '@types/minimist': specifier: 1.2.2 version: 1.2.2 @@ -111,6 +111,9 @@ importers: dotenv: specifier: 8.6.0 version: 8.6.0 + drizzle-orm: + specifier: 0.28.5 + version: 0.28.5(@libsql/client@0.3.4)(@types/pg@8.10.2)(pg@8.11.3) express: specifier: 4.18.2 version: 4.18.2 @@ -139,11 +142,11 @@ importers: specifier: 3.0.0 version: 3.0.0 jest: - specifier: 29.6.4 - version: 29.6.4(@types/node@20.5.7)(ts-node@10.9.1) + specifier: 29.7.0 + version: 29.7.0(@types/node@20.5.7)(ts-node@10.9.1) jest-environment-jsdom: - specifier: 29.6.4 - version: 29.6.4 + specifier: 29.7.0 + version: 29.7.0 jwt-decode: specifier: 3.1.2 version: 3.1.2 @@ -416,7 +419,7 @@ importers: version: 2.4.5 ts-jest: specifier: ^29.1.0 - version: 29.1.1(@babel/core@7.22.20)(jest@29.6.4)(typescript@5.2.2) + version: 29.1.1(@babel/core@7.22.20)(jest@29.7.0)(typescript@5.2.2) packages/db-mongodb: dependencies: @@ -517,7 +520,7 @@ importers: version: 2.28.1(@typescript-eslint/parser@6.6.0)(eslint@8.48.0) eslint-plugin-jest: specifier: 27.2.3 - version: 27.2.3(@typescript-eslint/eslint-plugin@6.6.0)(eslint@8.48.0)(jest@29.6.4)(typescript@5.2.2) + version: 27.2.3(@typescript-eslint/eslint-plugin@6.6.0)(eslint@8.48.0)(jest@29.7.0)(typescript@5.2.2) eslint-plugin-jest-dom: specifier: 5.1.0 version: 5.1.0(eslint@8.48.0) @@ -1072,7 +1075,7 @@ importers: version: link:../payload ts-jest: specifier: ^29.1.0 - version: 29.1.1(@babel/core@7.22.20)(jest@29.6.4)(typescript@5.2.2) + version: 29.1.1(@babel/core@7.22.20)(jest@29.7.0)(typescript@5.2.2) webpack: specifier: ^5.78.0 version: 5.88.2(@swc/core@1.3.76)(webpack-cli@4.10.0) @@ -3660,14 +3663,14 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.6.2 + '@types/node': 16.18.58 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.8.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.6.2)(ts-node@10.9.1) + jest-config: 29.7.0(@types/node@16.18.58)(ts-node@10.9.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -3701,7 +3704,7 @@ packages: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.6.2 + '@types/node': 16.18.58 jest-mock: 29.7.0 /@jest/expect-utils@29.7.0: @@ -3725,7 +3728,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.6.2 + '@types/node': 16.18.58 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -4150,7 +4153,6 @@ packages: - bufferutil - encoding - utf-8-validate - dev: false /@libsql/hrana-client@0.5.5: resolution: {integrity: sha512-i+hDBpiV719poqEiHupUUZYKJ9YSbCRFe5Q2PQ0v3mHIftePH6gayLjp2u6TXbqbO/Dv6y8yyvYlBXf/kFfRZA==} @@ -4163,7 +4165,6 @@ packages: - bufferutil - encoding - utf-8-validate - dev: false /@libsql/isomorphic-fetch@0.1.10: resolution: {integrity: sha512-dH0lMk50gKSvEKD78xWMu60SY1sjp1sY//iFLO0XMmBwfVfG136P9KOk06R4maBdlb8KMXOzJ1D28FR5ZKnHTA==} @@ -4172,7 +4173,6 @@ packages: node-fetch: 2.6.12 transitivePeerDependencies: - encoding - dev: false /@libsql/isomorphic-ws@0.1.5: resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} @@ -4182,7 +4182,6 @@ packages: transitivePeerDependencies: - bufferutil - utf-8-validate - dev: false /@mole-inc/bin-wrapper@8.0.1: resolution: {integrity: sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==} @@ -4376,12 +4375,12 @@ packages: engines: {node: '>=8.0.0'} dev: true - /@playwright/test@1.38.1: - resolution: {integrity: sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==} + /@playwright/test@1.39.0: + resolution: {integrity: sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==} engines: {node: '>=16'} hasBin: true dependencies: - playwright: 1.38.1 + playwright: 1.39.0 dev: true /@pnpm/config.env-replace@1.1.0: @@ -5832,6 +5831,13 @@ packages: pretty-format: 29.7.0 dev: true + /@types/jest@29.5.7: + resolution: {integrity: sha512-HLyetab6KVPSiF+7pFcUyMeLsx25LDNDemw9mGsJBkai/oouwrjTycocSDYopMEwFhN2Y4s9oPyOCZNofgSt2g==} + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + dev: true + /@types/joi@14.3.4: resolution: {integrity: sha512-1TQNDJvIKlgYXGNIABfgFp9y0FziDpuGrd799Q5RcnsDu+krD+eeW/0Fs5PHARvWWFelOhIG2OPCo6KbadBM4A==} dev: true @@ -5839,7 +5845,7 @@ packages: /@types/jsdom@20.0.1: resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} dependencies: - '@types/node': 20.6.2 + '@types/node': 16.18.58 '@types/tough-cookie': 4.0.3 parse5: 7.1.2 dev: true @@ -6174,7 +6180,7 @@ packages: /@types/testing-library__jest-dom@5.14.8: resolution: {integrity: sha512-NRfJE9Cgpmu4fx716q9SYmU4jxxhYRU1BQo239Txt/9N3EC745XZX1Yl7h/SBIDlo1ANVOCRB4YDXjaQdoKCHQ==} dependencies: - '@types/jest': 29.5.4 + '@types/jest': 29.5.7 dev: true /@types/to-snake-case@1.0.0: @@ -6267,7 +6273,6 @@ packages: resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==} dependencies: '@types/node': 16.18.58 - dev: false /@types/yargs-parser@21.0.0: resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -7193,7 +7198,6 @@ packages: dependencies: bindings: 1.5.0 prebuild-install: 7.1.1 - dev: false /big-integer@1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} @@ -7240,7 +7244,6 @@ packages: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} dependencies: file-uri-to-path: 1.0.0 - dev: false /bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -7391,7 +7394,6 @@ packages: /buffer-writer@2.0.0: resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} engines: {node: '>=4'} - dev: false /buffer@4.9.2: resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} @@ -8982,7 +8984,6 @@ packages: '@libsql/client': 0.3.4 '@types/pg': 8.10.2 pg: 8.11.3 - dev: false /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -9402,7 +9403,7 @@ packages: requireindex: 1.2.0 dev: false - /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.6.0)(eslint@8.48.0)(jest@29.6.4)(typescript@5.2.2): + /eslint-plugin-jest@27.2.3(@typescript-eslint/eslint-plugin@6.6.0)(eslint@8.48.0)(jest@29.7.0)(typescript@5.2.2): resolution: {integrity: sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -9418,7 +9419,7 @@ packages: '@typescript-eslint/eslint-plugin': 6.6.0(@typescript-eslint/parser@6.6.0)(eslint@8.48.0)(typescript@5.2.2) '@typescript-eslint/utils': 5.62.0(eslint@8.48.0)(typescript@5.2.2) eslint: 8.48.0 - jest: 29.6.4(@types/node@20.5.7)(ts-node@10.9.1) + jest: 29.7.0(@types/node@20.5.7)(ts-node@10.9.1) transitivePeerDependencies: - supports-color - typescript @@ -9501,7 +9502,7 @@ packages: optional: true dependencies: eslint: 8.48.0 - eslint-plugin-jest: 27.2.3(@typescript-eslint/eslint-plugin@6.6.0)(eslint@8.48.0)(jest@29.6.4)(typescript@5.2.2) + eslint-plugin-jest: 27.2.3(@typescript-eslint/eslint-plugin@6.6.0)(eslint@8.48.0)(jest@29.7.0)(typescript@5.2.2) dev: false /eslint-plugin-react-hooks@4.6.0(eslint@8.48.0): @@ -10001,7 +10002,6 @@ packages: /file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - dev: false /filename-reserved-regex@3.0.0: resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} @@ -11730,7 +11730,6 @@ packages: transitivePeerDependencies: - babel-plugin-macros - supports-color - dev: true /jest-config@29.7.0(@types/node@20.5.7)(ts-node@10.9.1): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} @@ -11772,46 +11771,6 @@ packages: - babel-plugin-macros - supports-color - /jest-config@29.7.0(@types/node@20.6.2)(ts-node@10.9.1): - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.22.20 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.6.2 - babel-jest: 29.7.0(@babel/core@7.22.20) - chalk: 4.1.2 - ci-info: 3.8.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - ts-node: 10.9.1(@swc/core@1.3.76)(@types/node@20.5.7)(typescript@5.2.2) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - /jest-diff@27.5.1: resolution: {integrity: sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -11847,8 +11806,8 @@ packages: jest-util: 29.7.0 pretty-format: 29.7.0 - /jest-environment-jsdom@29.6.4: - resolution: {integrity: sha512-K6wfgUJ16DoMs02JYFid9lOsqfpoVtyJxpRlnTxUHzvZWBnnh2VNGRB9EC1Cro96TQdq5TtSjb3qUjNaJP9IyA==} + /jest-environment-jsdom@29.7.0: + resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: canvas: ^2.5.0 @@ -11860,7 +11819,7 @@ packages: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 '@types/jsdom': 20.0.1 - '@types/node': 20.6.2 + '@types/node': 16.18.58 jest-mock: 29.7.0 jest-util: 29.7.0 jsdom: 20.0.3 @@ -11953,7 +11912,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 20.6.2 + '@types/node': 16.18.58 jest-util: 29.7.0 /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -12130,8 +12089,8 @@ packages: merge-stream: 2.0.0 supports-color: 8.1.1 - /jest@29.6.4(@types/node@16.18.58)(ts-node@10.9.1): - resolution: {integrity: sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==} + /jest@29.7.0(@types/node@16.18.58)(ts-node@10.9.1): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -12151,8 +12110,8 @@ packages: - ts-node dev: true - /jest@29.6.4(@types/node@20.5.7)(ts-node@10.9.1): - resolution: {integrity: sha512-tEFhVQFF/bzoYV1YuGyzLPZ6vlPrdfvDmmAxudA1dLEuiztqg2Rkx20vkKY32xiDROcD2KXlgZ7Cu8RPeEHRKw==} + /jest@29.7.0(@types/node@20.5.7)(ts-node@10.9.1): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true peerDependencies: @@ -12186,7 +12145,6 @@ packages: /js-base64@3.7.5: resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} - dev: false /js-beautify@1.14.9: resolution: {integrity: sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==} @@ -13330,7 +13288,6 @@ packages: data-uri-to-buffer: 4.0.1 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - dev: false /node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} @@ -13780,7 +13737,6 @@ packages: /packet-reader@1.0.0: resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} - dev: false /param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} @@ -14127,12 +14083,10 @@ packages: /pg-cloudflare@1.1.1: resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} requiresBuild: true - dev: false optional: true /pg-connection-string@2.6.2: resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} - dev: false /pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} @@ -14148,7 +14102,6 @@ packages: pg: '>=8.0' dependencies: pg: 8.11.3 - dev: false /pg-protocol@1.6.0: resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} @@ -14162,7 +14115,6 @@ packages: postgres-bytea: 1.0.0 postgres-date: 1.0.7 postgres-interval: 1.2.0 - dev: false /pg-types@4.0.1: resolution: {integrity: sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==} @@ -14194,13 +14146,11 @@ packages: pgpass: 1.0.5 optionalDependencies: pg-cloudflare: 1.1.1 - dev: false /pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} dependencies: split2: 4.2.0 - dev: false /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -14332,18 +14282,18 @@ packages: dependencies: find-up: 3.0.0 - /playwright-core@1.38.1: - resolution: {integrity: sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==} + /playwright-core@1.39.0: + resolution: {integrity: sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==} engines: {node: '>=16'} hasBin: true dev: true - /playwright@1.38.1: - resolution: {integrity: sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==} + /playwright@1.39.0: + resolution: {integrity: sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==} engines: {node: '>=16'} hasBin: true dependencies: - playwright-core: 1.38.1 + playwright-core: 1.39.0 optionalDependencies: fsevents: 2.3.2 dev: true @@ -14981,7 +14931,6 @@ packages: /postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} - dev: false /postgres-array@3.0.2: resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} @@ -14990,7 +14939,6 @@ packages: /postgres-bytea@1.0.0: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} engines: {node: '>=0.10.0'} - dev: false /postgres-bytea@3.0.0: resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} @@ -15001,7 +14949,6 @@ packages: /postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} engines: {node: '>=0.10.0'} - dev: false /postgres-date@2.0.1: resolution: {integrity: sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==} @@ -15012,7 +14959,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: xtend: 4.0.2 - dev: false /postgres-interval@3.0.0: resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} @@ -17194,7 +17140,7 @@ packages: dependencies: typescript: 5.2.2 - /ts-jest@29.1.1(@babel/core@7.22.20)(jest@29.6.4)(typescript@5.2.2): + /ts-jest@29.1.1(@babel/core@7.22.20)(jest@29.7.0)(typescript@5.2.2): resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -17218,7 +17164,7 @@ packages: '@babel/core': 7.22.20 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.6.4(@types/node@16.18.58)(ts-node@10.9.1) + jest: 29.7.0(@types/node@16.18.58)(ts-node@10.9.1) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 diff --git a/test/admin/collections/CustomViews1.ts b/test/admin/collections/CustomViews1.ts index 623b6ee169..1495d707ee 100644 --- a/test/admin/collections/CustomViews1.ts +++ b/test/admin/collections/CustomViews1.ts @@ -1,11 +1,10 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' import CustomEditView from '../components/views/CustomEdit' - -export const customViews1Slug = 'custom-views-one' +import { customViews1CollectionSlug } from '../slugs' export const CustomViews1: CollectionConfig = { - slug: customViews1Slug, + slug: customViews1CollectionSlug, versions: true, admin: { components: { diff --git a/test/admin/collections/CustomViews2.ts b/test/admin/collections/CustomViews2.ts index d39b880459..b7d5eee657 100644 --- a/test/admin/collections/CustomViews2.ts +++ b/test/admin/collections/CustomViews2.ts @@ -10,11 +10,11 @@ import { customNestedTabViewPath, customTabLabel, customTabViewPath, - customViews2Slug, } from '../shared' +import { customViews2CollectionSlug } from '../slugs' export const CustomViews2: CollectionConfig = { - slug: customViews2Slug, + slug: customViews2CollectionSlug, versions: true, admin: { components: { diff --git a/test/admin/collections/Geo.ts b/test/admin/collections/Geo.ts index 14029e6a35..4756531c7d 100644 --- a/test/admin/collections/Geo.ts +++ b/test/admin/collections/Geo.ts @@ -1,7 +1,9 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' +import { geoCollectionSlug } from '../slugs' + export const Geo: CollectionConfig = { - slug: 'geo', + slug: geoCollectionSlug, fields: [ { name: 'point', diff --git a/test/admin/collections/Group1A.ts b/test/admin/collections/Group1A.ts index 0cd55d9a34..b34fef4d1d 100644 --- a/test/admin/collections/Group1A.ts +++ b/test/admin/collections/Group1A.ts @@ -1,6 +1,6 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' -import { group1Collection1Slug } from '../shared' +import { group1Collection1Slug } from '../slugs' export const CollectionGroup1A: CollectionConfig = { slug: group1Collection1Slug, diff --git a/test/admin/collections/Group1B.ts b/test/admin/collections/Group1B.ts index 7334d60145..279923adb3 100644 --- a/test/admin/collections/Group1B.ts +++ b/test/admin/collections/Group1B.ts @@ -1,6 +1,6 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' -import { group1Collection2Slug } from '../shared' +import { group1Collection2Slug } from '../slugs' export const CollectionGroup1B: CollectionConfig = { slug: group1Collection2Slug, diff --git a/test/admin/collections/Group2A.ts b/test/admin/collections/Group2A.ts index 4444807614..e05c134509 100644 --- a/test/admin/collections/Group2A.ts +++ b/test/admin/collections/Group2A.ts @@ -1,7 +1,9 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' +import { group2Collection1Slug } from '../slugs' + export const CollectionGroup2A: CollectionConfig = { - slug: 'group-two-collection-ones', + slug: group2Collection1Slug, admin: { group: 'One', }, diff --git a/test/admin/collections/Group2B.ts b/test/admin/collections/Group2B.ts index bcb9928fb2..417291a0be 100644 --- a/test/admin/collections/Group2B.ts +++ b/test/admin/collections/Group2B.ts @@ -1,7 +1,9 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' +import { group2Collection2Slug } from '../slugs' + export const CollectionGroup2B: CollectionConfig = { - slug: 'group-two-collection-twos', + slug: group2Collection2Slug, admin: { group: 'One', }, diff --git a/test/admin/collections/Hidden.ts b/test/admin/collections/Hidden.ts index 0da1f7c947..de85553932 100644 --- a/test/admin/collections/Hidden.ts +++ b/test/admin/collections/Hidden.ts @@ -1,7 +1,9 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' +import { hiddenCollectionSlug } from '../slugs' + export const CollectionHidden: CollectionConfig = { - slug: 'hidden-collection', + slug: hiddenCollectionSlug, admin: { hidden: () => true, }, diff --git a/test/admin/collections/NoApiView.ts b/test/admin/collections/NoApiView.ts index 31690b005e..d11a5aa2fe 100644 --- a/test/admin/collections/NoApiView.ts +++ b/test/admin/collections/NoApiView.ts @@ -1,9 +1,9 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' -import { noApiViewCollection } from '../shared' +import { noApiViewCollectionSlug } from '../slugs' export const CollectionNoApiView: CollectionConfig = { - slug: noApiViewCollection, + slug: noApiViewCollectionSlug, admin: { hideAPIURL: true, }, diff --git a/test/admin/collections/Posts.ts b/test/admin/collections/Posts.ts index 1484616b23..48e6295bb5 100644 --- a/test/admin/collections/Posts.ts +++ b/test/admin/collections/Posts.ts @@ -3,10 +3,11 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections import { slateEditor } from '../../../packages/richtext-slate/src' import DemoUIFieldCell from '../components/DemoUIField/Cell' import DemoUIFieldField from '../components/DemoUIField/Field' -import { postsSlug, slugPluralLabel, slugSingularLabel } from '../shared' +import { slugPluralLabel, slugSingularLabel } from '../shared' +import { postsCollectionSlug } from '../slugs' export const Posts: CollectionConfig = { - slug: postsSlug, + slug: postsCollectionSlug, labels: { singular: slugSingularLabel, plural: slugPluralLabel, diff --git a/test/admin/collections/Users.ts b/test/admin/collections/Users.ts index 2ea64af34c..732691a0fd 100644 --- a/test/admin/collections/Users.ts +++ b/test/admin/collections/Users.ts @@ -1,7 +1,9 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' +import { usersCollectionSlug } from '../slugs' + export const Users: CollectionConfig = { - slug: 'users', + slug: usersCollectionSlug, auth: true, admin: { useAsTitle: 'email', diff --git a/test/admin/config.ts b/test/admin/config.ts index 825cbface9..6921f3d00f 100644 --- a/test/admin/config.ts +++ b/test/admin/config.ts @@ -27,7 +27,7 @@ import { GlobalGroup1A } from './globals/Group1A' import { GlobalGroup1B } from './globals/Group1B' import { GlobalHidden } from './globals/Hidden' import { GlobalNoApiView } from './globals/NoApiView' -import { seed } from './seed' +import { clearAndSeedEverything } from './seed' import { customNestedViewPath, customViewPath } from './shared' export default buildConfigWithDefaults({ @@ -63,6 +63,16 @@ export default buildConfigWithDefaults({ }, }, }, + webpack: (config) => ({ + ...config, + resolve: { + ...config.resolve, + alias: { + ...config?.resolve?.alias, + fs: path.resolve(__dirname, './mocks/emptyModule.js'), + }, + }, + }), }, i18n: { resources: { @@ -99,5 +109,7 @@ export default buildConfigWithDefaults({ GlobalGroup1A, GlobalGroup1B, ], - onInit: seed, + onInit: async (payload) => { + await clearAndSeedEverything(payload) + }, }) diff --git a/test/admin/e2e.spec.ts b/test/admin/e2e.spec.ts index 8731cc6a12..d5d352aa1c 100644 --- a/test/admin/e2e.spec.ts +++ b/test/admin/e2e.spec.ts @@ -3,7 +3,6 @@ import type { Page } from '@playwright/test' import { expect, test } from '@playwright/test' import qs from 'qs' -import type { PayloadRequest } from '../../packages/payload/src/express/types' import type { Post } from './payload-types' import payload from '../../packages/payload/src' @@ -20,6 +19,7 @@ import { } from '../helpers' import { AdminUrlUtil } from '../helpers/adminUrlUtil' import { initPayloadE2E } from '../helpers/configHelpers' +import { clearAndSeedEverything } from './seed' import { customEditLabel, customNestedTabViewPath, @@ -31,17 +31,19 @@ import { customTabViewTitle, customViewPath, customViewTitle, - customViews2Slug, + slugPluralLabel, +} from './shared' +import { + customViews2CollectionSlug, globalSlug, group1Collection1Slug, group1GlobalSlug, - noApiViewCollection, - noApiViewGlobal, - postsSlug, - slugPluralLabel, -} from './shared' + noApiViewCollectionSlug, + noApiViewGlobalSlug, + postsCollectionSlug, +} from './slugs' -const { afterEach, beforeAll, beforeEach, describe } = test +const { beforeAll, beforeEach, describe } = test const title = 'Title' const description = 'Description' @@ -54,29 +56,21 @@ describe('admin', () => { beforeAll(async ({ browser }) => { serverURL = (await initPayloadE2E(__dirname)).serverURL - await clearDocs() // Clear any seeded data from onInit - url = new AdminUrlUtil(serverURL, postsSlug) - customViewsURL = new AdminUrlUtil(serverURL, customViews2Slug) + url = new AdminUrlUtil(serverURL, postsCollectionSlug) + customViewsURL = new AdminUrlUtil(serverURL, customViews2CollectionSlug) const context = await browser.newContext() page = await context.newPage() }) - - afterEach(async () => { - await clearDocs() - // clear preferences - await payload.db.deleteMany({ - collection: 'payload-preferences', - req: {} as PayloadRequest, - where: {}, - }) + beforeEach(async () => { + await clearAndSeedEverything(payload) }) describe('Nav', () => { test('should nav to collection - nav', async () => { await page.goto(url.admin) await openNav(page) - await page.locator(`#nav-${postsSlug}`).click() + await page.locator(`#nav-${postsCollectionSlug}`).click() expect(page.url()).toContain(url.list) }) @@ -90,7 +84,7 @@ describe('admin', () => { test('should navigate to collection - card', async () => { await page.goto(url.admin) await wait(200) - await page.locator(`#card-${postsSlug}`).click() + await page.locator(`#card-${postsCollectionSlug}`).click() expect(page.url()).toContain(url.list) }) @@ -147,7 +141,7 @@ describe('admin', () => { const { id } = await createPost() await page.goto(url.edit(id)) const collectionBreadcrumb = page.locator( - `.step-nav a[href="/admin/collections/${postsSlug}"]`, + `.step-nav a[href="/admin/collections/${postsCollectionSlug}"]`, ) await expect(collectionBreadcrumb).toBeVisible() await expect(collectionBreadcrumb).toHaveText(slugPluralLabel) @@ -239,35 +233,35 @@ describe('admin', () => { }) test('collection - should not show API tab when disabled in config', async () => { - await page.goto(url.collection(noApiViewCollection)) + await page.goto(url.collection(noApiViewCollectionSlug)) await page.locator('.collection-list .table a').click() await expect(page.locator('.doc-tabs__tabs-container')).not.toContainText('API') }) test('collection - should not enable API route when disabled in config', async () => { const collectionItems = await payload.find({ - collection: noApiViewCollection, + collection: noApiViewCollectionSlug, limit: 1, }) expect(collectionItems.docs.length).toBe(1) - await page.goto(`${url.collection(noApiViewCollection)}/${collectionItems.docs[0].id}/api`) + await page.goto(`${url.collection(noApiViewGlobalSlug)}/${collectionItems.docs[0].id}/api`) await expect(page.locator('.not-found')).toHaveCount(1) }) test('global - should not show API tab when disabled in config', async () => { - await page.goto(url.global(noApiViewGlobal)) + await page.goto(url.global(noApiViewGlobalSlug)) await expect(page.locator('.doc-tabs__tabs-container')).not.toContainText('API') }) test('global - should not enable API route when disabled in config', async () => { - await page.goto(`${url.global(noApiViewGlobal)}/api`) + await page.goto(`${url.global(noApiViewGlobalSlug)}/api`) await expect(page.locator('.not-found')).toHaveCount(1) }) }) describe('ui', () => { test('collection - should render preview button when `admin.preview` is set', async () => { - const collectionWithPreview = new AdminUrlUtil(serverURL, postsSlug) + const collectionWithPreview = new AdminUrlUtil(serverURL, postsCollectionSlug) await page.goto(collectionWithPreview.create) await page.locator('#field-title').fill(title) await saveDocAndAssert(page) @@ -406,9 +400,11 @@ describe('admin', () => { }) test('should bulk delete', async () => { - await createPost() - await createPost() - await createPost() + // First, delete all posts created by the seed + await deleteAllPosts() + + await Promise.all([createPost(), createPost(), createPost()]) + await page.goto(url.list) await page.locator('input#select-all').check() await page.locator('.delete-documents__toggle').click() @@ -420,9 +416,10 @@ describe('admin', () => { }) test('should bulk update', async () => { - await createPost() - await createPost() - await createPost() + // First, delete all posts created by the seed + await deleteAllPosts() + + await Promise.all([createPost(), createPost(), createPost()]) const bulkTitle = 'Bulk update title' await page.goto(url.list) @@ -514,6 +511,9 @@ describe('admin', () => { }) test('search by id', async () => { + // delete all posts created by the seed + await deleteAllPosts() + const { id } = await createPost() await page.locator('.search-filter__input').fill(id) const tableItems = page.locator(tableRowLocator) @@ -534,6 +534,9 @@ describe('admin', () => { }) test('toggle columns', async () => { + // delete all posts created by the seed + await deleteAllPosts() + const columnCountLocator = 'table > thead > tr > th' await createPost() @@ -592,6 +595,9 @@ describe('admin', () => { }) test('filter rows', async () => { + // delete all posts created by the seed + await deleteAllPosts() + const { id } = await createPost({ title: 'post1' }) await createPost({ title: 'post2' }) @@ -923,6 +929,9 @@ describe('admin', () => { describe('multi-select', () => { beforeEach(async () => { + // delete all posts created by the seed + await deleteAllPosts() + await mapAsync([...Array(3)], async () => { await createPost() }) @@ -962,12 +971,6 @@ describe('admin', () => { }) describe('pagination', () => { - beforeAll(async () => { - await mapAsync([...Array(11)], async () => { - await createPost() - }) - }) - test('should paginate', async () => { const pageInfo = page.locator('.collection-list__page-info') const perPage = page.locator('.per-page') @@ -999,13 +1002,18 @@ describe('admin', () => { // TODO: Troubleshoot flaky suite describe('sorting', () => { - beforeAll(async () => { - await createPost({ - number: 1, - }) - await createPost({ - number: 2, - }) + beforeEach(async () => { + // delete all posts created by the seed + await deleteAllPosts() + + await Promise.all([ + createPost({ + number: 1, + }), + createPost({ + number: 2, + }), + ]) }) test('should sort', async () => { @@ -1082,18 +1090,26 @@ describe('admin', () => { async function createPost(overrides?: Partial): Promise { return payload.create({ - collection: postsSlug, + collection: postsCollectionSlug, data: { description, title, ...overrides, }, - }) + }) as unknown as Promise } -async function clearDocs(): Promise { - await payload.delete({ - collection: postsSlug, - where: { id: { exists: true } }, +async function deleteAllPosts() { + const posts = await payload.find({ + collection: postsCollectionSlug, + limit: 100, }) + await Promise.all([ + ...posts.docs.map((post) => { + return payload.delete({ + collection: postsCollectionSlug, + id: post.id, + }) + }), + ]) } diff --git a/test/admin/globals/CustomViews1.ts b/test/admin/globals/CustomViews1.ts index ddfcc9a73c..d613630eb1 100644 --- a/test/admin/globals/CustomViews1.ts +++ b/test/admin/globals/CustomViews1.ts @@ -1,9 +1,10 @@ import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' import CustomEditView from '../components/views/CustomEdit' +import { customGlobalViews1GlobalSlug } from '../slugs' export const CustomGlobalViews1: GlobalConfig = { - slug: 'custom-global-views-one', + slug: customGlobalViews1GlobalSlug, versions: true, admin: { components: { diff --git a/test/admin/globals/CustomViews2.ts b/test/admin/globals/CustomViews2.ts index 6c3339f4fd..60c8a482c4 100644 --- a/test/admin/globals/CustomViews2.ts +++ b/test/admin/globals/CustomViews2.ts @@ -4,9 +4,10 @@ import CustomTabComponent from '../components/CustomTabComponent' import CustomDefaultEditView from '../components/views/CustomEditDefault' import CustomView from '../components/views/CustomTab' import CustomVersionsView from '../components/views/CustomVersions' +import { customGlobalViews2GlobalSlug } from '../slugs' export const CustomGlobalViews2: GlobalConfig = { - slug: 'custom-global-views-two', + slug: customGlobalViews2GlobalSlug, versions: true, admin: { components: { diff --git a/test/admin/globals/Global.ts b/test/admin/globals/Global.ts index 20f984b3b6..90d3090e06 100644 --- a/test/admin/globals/Global.ts +++ b/test/admin/globals/Global.ts @@ -1,6 +1,6 @@ import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' -import { globalSlug } from '../shared' +import { globalSlug } from '../slugs' export const Global: GlobalConfig = { slug: globalSlug, diff --git a/test/admin/globals/Group1A.ts b/test/admin/globals/Group1A.ts index 0681ac156d..4c83a1b905 100644 --- a/test/admin/globals/Group1A.ts +++ b/test/admin/globals/Group1A.ts @@ -1,6 +1,6 @@ import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' -import { group1GlobalSlug } from '../shared' +import { group1GlobalSlug } from '../slugs' export const GlobalGroup1A: GlobalConfig = { slug: group1GlobalSlug, diff --git a/test/admin/globals/Group1B.ts b/test/admin/globals/Group1B.ts index 861091ee85..58403f00f0 100644 --- a/test/admin/globals/Group1B.ts +++ b/test/admin/globals/Group1B.ts @@ -1,7 +1,9 @@ import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' +import { group2GlobalSlug } from '../slugs' + export const GlobalGroup1B: GlobalConfig = { - slug: 'group-globals-two', + slug: group2GlobalSlug, admin: { group: 'Group', }, diff --git a/test/admin/globals/Hidden.ts b/test/admin/globals/Hidden.ts index fed2d8caa8..8718d87a85 100644 --- a/test/admin/globals/Hidden.ts +++ b/test/admin/globals/Hidden.ts @@ -1,7 +1,9 @@ import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' +import { hiddenGlobalSlug } from '../slugs' + export const GlobalHidden: GlobalConfig = { - slug: 'hidden-global', + slug: hiddenGlobalSlug, admin: { hidden: () => true, }, diff --git a/test/admin/globals/NoApiView.ts b/test/admin/globals/NoApiView.ts index d6eb30902e..93bdbeda17 100644 --- a/test/admin/globals/NoApiView.ts +++ b/test/admin/globals/NoApiView.ts @@ -1,8 +1,9 @@ import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' -import { noApiViewGlobal } from '../shared' + +import { noApiViewGlobalSlug } from '../slugs' export const GlobalNoApiView: GlobalConfig = { - slug: noApiViewGlobal, + slug: noApiViewGlobalSlug, admin: { hideAPIURL: true, }, diff --git a/test/admin/mocks/emptyModule.js b/test/admin/mocks/emptyModule.js new file mode 100644 index 0000000000..b1c6ea436a --- /dev/null +++ b/test/admin/mocks/emptyModule.js @@ -0,0 +1 @@ +export default {} diff --git a/test/admin/seed.ts b/test/admin/seed.ts new file mode 100644 index 0000000000..49466d9583 --- /dev/null +++ b/test/admin/seed.ts @@ -0,0 +1,70 @@ +import type { Payload } from '../../packages/payload/src' + +import { devUser } from '../credentials' +import { seedDB } from '../helpers/seed' +import { + collectionSlugs, + customViews1CollectionSlug, + customViews2CollectionSlug, + geoCollectionSlug, + noApiViewCollectionSlug, + postsCollectionSlug, + usersCollectionSlug, +} from './slugs' + +export async function clearAndSeedEverything(_payload: Payload) { + return await seedDB({ + snapshotKey: 'adminTest', + shouldResetDB: true, + collectionSlugs, + _payload, + seedFunction: async (_payload) => { + await Promise.all([ + _payload.create({ + collection: usersCollectionSlug, + data: { + email: devUser.email, + password: devUser.password, + }, + }), + ...[...Array(11)].map(() => { + _payload.create({ + collection: postsCollectionSlug, + data: { + title: 'Title', + description: 'Description', + }, + }) + }), + _payload.create({ + collection: customViews1CollectionSlug, + data: { + title: 'Custom View', + }, + }), + _payload.create({ + collection: customViews2CollectionSlug, + data: { + title: 'Custom View', + }, + }), + _payload.create({ + collection: geoCollectionSlug, + data: { + point: [7, -7], + }, + }), + _payload.create({ + collection: geoCollectionSlug, + data: { + point: [5, -5], + }, + }), + _payload.create({ + collection: noApiViewCollectionSlug, + data: {}, + }), + ]) + }, + }) +} diff --git a/test/admin/seed/index.ts b/test/admin/seed/index.ts deleted file mode 100644 index e2cedfa63d..0000000000 --- a/test/admin/seed/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { Config } from '../../../packages/payload/src/config/types' - -import { mapAsync } from '../../../packages/payload/src/utilities/mapAsync' -import { devUser } from '../../credentials' -import { customViews1Slug } from '../collections/CustomViews1' -import { customViews2Slug, noApiViewCollection, postsSlug } from '../shared' - -export const seed: Config['onInit'] = async (payload) => { - await payload.create({ - collection: 'users', - data: { - email: devUser.email, - password: devUser.password, - }, - }) - - await mapAsync([...Array(11)], async () => { - await payload.create({ - collection: postsSlug, - data: { - title: 'Title', - description: 'Description', - }, - }) - }) - - await payload.create({ - collection: customViews1Slug, - data: { - title: 'Custom View', - }, - }) - - await payload.create({ - collection: customViews2Slug, - data: { - title: 'Custom View', - }, - }) - - await payload.create({ - collection: 'geo', - data: { - point: [7, -7], - }, - }) - - await payload.create({ - collection: 'geo', - data: { - point: [5, -5], - }, - }) - - await payload.create({ - collection: noApiViewCollection, - data: {}, - }) -} diff --git a/test/admin/shared.ts b/test/admin/shared.ts index 5005c7b9f3..ef61776f21 100644 --- a/test/admin/shared.ts +++ b/test/admin/shared.ts @@ -1,21 +1,7 @@ -export const postsSlug = 'posts' - -export const group1Collection1Slug = 'group-one-collection-ones' - -export const group1Collection2Slug = 'group-one-collection-twos' - export const slugSingularLabel = 'Post' export const slugPluralLabel = 'Posts' -export const globalSlug = 'global' - -export const group1GlobalSlug = 'group-globals-one' - -export const noApiViewCollection = 'collection-no-api-view' - -export const noApiViewGlobal = 'global-no-api-view' - export const customViewPath = '/custom-view' export const customViewTitle = 'Custom View' @@ -24,8 +10,6 @@ export const customNestedViewPath = `${customViewPath}/nested-view` export const customNestedViewTitle = 'Custom Nested View' -export const customViews2Slug = 'custom-views-two' - export const customEditLabel = 'Custom Edit Label' export const customTabLabel = 'Custom Tab Label' diff --git a/test/admin/slugs.ts b/test/admin/slugs.ts new file mode 100644 index 0000000000..7c6e580adc --- /dev/null +++ b/test/admin/slugs.ts @@ -0,0 +1,41 @@ +export const usersCollectionSlug = 'users' as const +export const customViews1CollectionSlug = 'custom-views-one' as const +export const customViews2CollectionSlug = 'custom-views-two' as const +export const geoCollectionSlug = 'geo' as const +export const postsCollectionSlug = 'posts' as const +export const group1Collection1Slug = 'group-one-collection-ones' as const +export const group1Collection2Slug = 'group-one-collection-twos' as const +export const group2Collection1Slug = 'group-two-collection-ones' as const +export const group2Collection2Slug = 'group-two-collection-twos' as const +export const hiddenCollectionSlug = 'hidden-collection' as const +export const noApiViewCollectionSlug = 'collection-no-api-view' as const +export const collectionSlugs = [ + usersCollectionSlug, + customViews1CollectionSlug, + customViews2CollectionSlug, + geoCollectionSlug, + postsCollectionSlug, + group1Collection1Slug, + group1Collection2Slug, + group2Collection1Slug, + group2Collection2Slug, + hiddenCollectionSlug, + noApiViewCollectionSlug, +] + +export const customGlobalViews1GlobalSlug = 'custom-global-views-one' as const +export const customGlobalViews2GlobalSlug = 'custom-global-views-two' as const +export const globalSlug = 'global' as const +export const group1GlobalSlug = 'group-globals-one' as const +export const group2GlobalSlug = 'group-globals-two' as const +export const hiddenGlobalSlug = 'hidden-global' as const +export const noApiViewGlobalSlug = 'global-no-api-view' +export const globalSlugs = [ + customGlobalViews1GlobalSlug, + customGlobalViews2GlobalSlug, + globalSlug, + group1GlobalSlug, + group2GlobalSlug, + hiddenGlobalSlug, + noApiViewGlobalSlug, +] diff --git a/test/fields/collections/Array/index.ts b/test/fields/collections/Array/index.ts index 5d660c8399..63092e07f5 100644 --- a/test/fields/collections/Array/index.ts +++ b/test/fields/collections/Array/index.ts @@ -1,11 +1,10 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { arrayFieldsSlug } from '../../slugs' import { ArrayRowLabel } from './LabelComponent' export const arrayDefaultValue = [{ text: 'row one' }, { text: 'row two' }] -export const arrayFieldsSlug = 'array-fields' - const ArrayFields: CollectionConfig = { slug: arrayFieldsSlug, admin: { diff --git a/test/fields/collections/Blocks/index.ts b/test/fields/collections/Blocks/index.ts index 2743a97f13..377c72c09a 100644 --- a/test/fields/collections/Blocks/index.ts +++ b/test/fields/collections/Blocks/index.ts @@ -1,6 +1,7 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' import type { BlockField } from '../../../../packages/payload/src/fields/config/types' +import { blockFieldsSlug } from '../../slugs' import { AddCustomBlocks } from './components/AddCustomBlocks' export const getBlocksFieldSeedData = (prefix?: string): any => [ @@ -149,7 +150,7 @@ export const getBlocksField = (prefix?: string): BlockField => ({ }) const BlockFields: CollectionConfig = { - slug: 'block-fields', + slug: blockFieldsSlug, fields: [ getBlocksField(), { diff --git a/test/fields/collections/Checkbox/index.ts b/test/fields/collections/Checkbox/index.ts index 61ee700500..b7e38b0f11 100644 --- a/test/fields/collections/Checkbox/index.ts +++ b/test/fields/collections/Checkbox/index.ts @@ -1,7 +1,9 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { checkboxFieldsSlug } from '../../slugs' + const CheckboxFields: CollectionConfig = { - slug: 'checkbox-fields', + slug: checkboxFieldsSlug, fields: [ { name: 'checkbox', diff --git a/test/fields/collections/Code/index.tsx b/test/fields/collections/Code/index.tsx index 0e9bcb8e26..67562185ed 100644 --- a/test/fields/collections/Code/index.tsx +++ b/test/fields/collections/Code/index.tsx @@ -1,6 +1,8 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' import type { CodeField } from '../../payload-types' +import { codeFieldsSlug } from '../../slugs' + const Code: CollectionConfig = { fields: [ { @@ -39,7 +41,7 @@ const Code: CollectionConfig = { type: 'code', }, ], - slug: 'code-fields', + slug: codeFieldsSlug, } export const codeDoc: Partial = { diff --git a/test/fields/collections/Collapsible/index.ts b/test/fields/collections/Collapsible/index.ts index 891e4a7f84..bb4776c14d 100644 --- a/test/fields/collections/Collapsible/index.ts +++ b/test/fields/collections/Collapsible/index.ts @@ -1,7 +1,7 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { collapsibleFieldsSlug } from '../../slugs' import { CollapsibleLabelComponent } from './LabelComponent' -import { collapsibleFieldsSlug } from './shared' const CollapsibleFields: CollectionConfig = { slug: collapsibleFieldsSlug, diff --git a/test/fields/collections/Collapsible/shared.ts b/test/fields/collections/Collapsible/shared.ts deleted file mode 100644 index f22c00d937..0000000000 --- a/test/fields/collections/Collapsible/shared.ts +++ /dev/null @@ -1 +0,0 @@ -export const collapsibleFieldsSlug = 'collapsible-fields' diff --git a/test/fields/collections/ConditionalLogic/index.ts b/test/fields/collections/ConditionalLogic/index.ts index 3497b1bad8..989786016e 100644 --- a/test/fields/collections/ConditionalLogic/index.ts +++ b/test/fields/collections/ConditionalLogic/index.ts @@ -1,7 +1,9 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { conditionalLogicSlug } from '../../slugs' + const ConditionalLogic: CollectionConfig = { - slug: 'conditional-logic', + slug: conditionalLogicSlug, admin: { useAsTitle: 'text', }, diff --git a/test/fields/collections/Date/index.ts b/test/fields/collections/Date/index.ts index 078f6068a8..9be67b4864 100644 --- a/test/fields/collections/Date/index.ts +++ b/test/fields/collections/Date/index.ts @@ -1,9 +1,11 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { dateFieldsSlug } from '../../slugs' + export const defaultText = 'default-text' const DateFields: CollectionConfig = { - slug: 'date-fields', + slug: dateFieldsSlug, admin: { useAsTitle: 'default', }, diff --git a/test/fields/collections/Group/index.ts b/test/fields/collections/Group/index.ts index a14e78240d..32f0a8db7c 100644 --- a/test/fields/collections/Group/index.ts +++ b/test/fields/collections/Group/index.ts @@ -1,8 +1,9 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { groupFieldsSlug } from '../../slugs' + export const groupDefaultValue = 'set from parent' export const groupDefaultChild = 'child takes priority' -export const groupFieldsSlug = 'group-fields' const GroupFields: CollectionConfig = { slug: groupFieldsSlug, diff --git a/test/fields/collections/Indexed/index.ts b/test/fields/collections/Indexed/index.ts index 15a93e4049..66949a2336 100644 --- a/test/fields/collections/Indexed/index.ts +++ b/test/fields/collections/Indexed/index.ts @@ -4,6 +4,8 @@ import type { } from '../../../../packages/payload/src/collections/config/types' import type { IndexedField } from '../../payload-types' +import { indexedFieldsSlug } from '../../slugs' + const beforeDuplicate: BeforeDuplicate = ({ data }) => { return { ...data, @@ -22,7 +24,7 @@ const beforeDuplicate: BeforeDuplicate = ({ data }) => { } const IndexedFields: CollectionConfig = { - slug: 'indexed-fields', + slug: indexedFieldsSlug, // used to assert that versions also get indexes admin: { hooks: { diff --git a/test/fields/collections/JSON/index.tsx b/test/fields/collections/JSON/index.tsx index 44bb96b821..4ab19dd0e9 100644 --- a/test/fields/collections/JSON/index.tsx +++ b/test/fields/collections/JSON/index.tsx @@ -1,5 +1,7 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { jsonFieldsSlug } from '../../slugs' + type JSONField = { createdAt: string id: string @@ -17,7 +19,7 @@ const JSON: CollectionConfig = { type: 'json', }, ], - slug: 'json-fields', + slug: jsonFieldsSlug, versions: { maxPerDoc: 1, }, diff --git a/test/fields/collections/Lexical/data.ts b/test/fields/collections/Lexical/data.ts new file mode 100644 index 0000000000..ac76364509 --- /dev/null +++ b/test/fields/collections/Lexical/data.ts @@ -0,0 +1,8 @@ +import { generateLexicalRichText } from './generateLexicalRichText' +import { payloadPluginLexicalData } from './generatePayloadPluginLexicalData' + +export const lexicalRichTextDoc = { + title: 'Rich Text', + richTextLexicalCustomFields: generateLexicalRichText(), + richTextLexicalWithLexicalPluginData: payloadPluginLexicalData, +} diff --git a/test/fields/collections/Lexical/index.ts b/test/fields/collections/Lexical/index.ts index 599e0e1692..a88780a092 100644 --- a/test/fields/collections/Lexical/index.ts +++ b/test/fields/collections/Lexical/index.ts @@ -5,10 +5,12 @@ import { HTMLConverterFeature, LexicalPluginToLexicalFeature, LinkFeature, + TestRecorderFeature, TreeviewFeature, UploadFeature, lexicalEditor, } from '../../../../packages/richtext-lexical/src' +import { lexicalFieldsSlug } from '../../slugs' import { RelationshipBlock, RichTextBlock, @@ -21,7 +23,7 @@ import { generateLexicalRichText } from './generateLexicalRichText' import { payloadPluginLexicalData } from './generatePayloadPluginLexicalData' export const LexicalFields: CollectionConfig = { - slug: 'lexical-fields', + slug: lexicalFieldsSlug, admin: { useAsTitle: 'title', listSearchableFields: ['title', 'richTextLexicalCustomFields'], @@ -35,6 +37,27 @@ export const LexicalFields: CollectionConfig = { type: 'text', required: true, }, + { + name: 'richTextLexicalSimple', + type: 'richText', + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + TestRecorderFeature(), + TreeviewFeature(), + BlocksFeature({ + blocks: [ + RichTextBlock, + TextBlock, + UploadAndRichTextBlock, + SelectFieldBlock, + RelationshipBlock, + SubBlockBlock, + ], + }), + ], + }), + }, { name: 'richTextLexicalCustomFields', type: 'richText', @@ -42,6 +65,7 @@ export const LexicalFields: CollectionConfig = { editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, + TestRecorderFeature(), TreeviewFeature(), HTMLConverterFeature(), LinkFeature({ @@ -126,9 +150,3 @@ export const LexicalFields: CollectionConfig = { }, ], } - -export const LexicalRichTextDoc = { - title: 'Rich Text', - richTextLexicalCustomFields: generateLexicalRichText(), - richTextLexicalWithLexicalPluginData: payloadPluginLexicalData, -} diff --git a/test/fields/collections/LexicalMigrate/generatePayloadPluginLexicalData.ts b/test/fields/collections/LexicalMigrate/generatePayloadPluginLexicalData.ts new file mode 100644 index 0000000000..31aacd6b40 --- /dev/null +++ b/test/fields/collections/LexicalMigrate/generatePayloadPluginLexicalData.ts @@ -0,0 +1,958 @@ +export const payloadPluginLexicalData = { + words: 49, + preview: + 'paragraph text bold italic underline and all subscript superscript code internal link external link…', + comments: [], + characters: 493, + jsonContent: { + root: { + type: 'root', + format: '', + indent: 0, + version: 1, + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'paragraph text ', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 1, + mode: 'normal', + style: '', + text: 'bold', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: ' ', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 2, + mode: 'normal', + style: '', + text: 'italic', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: ' ', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 8, + mode: 'normal', + style: '', + text: 'underline', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: ' and ', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 11, + mode: 'normal', + style: '', + text: 'all', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: ' ', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 32, + mode: 'normal', + style: '', + text: 'subscript', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: ' ', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 64, + mode: 'normal', + style: '', + text: 'superscript', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: ' ', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 16, + mode: 'normal', + style: '', + text: 'code', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: ' ', + type: 'text', + version: 1, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'internal link', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'link', + version: 2, + attributes: { + newTab: true, + linkType: 'internal', + doc: { + value: '{{TEXT_DOC_ID}}', + relationTo: 'text-fields', + data: {}, // populated data + }, + text: 'internal link', + }, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: ' ', + type: 'text', + version: 1, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'external link', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'link', + version: 2, + attributes: { + newTab: true, + nofollow: false, + url: 'https://fewfwef.de', + linkType: 'custom', + text: 'external link', + }, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: ' s. ', + type: 'text', + version: 1, + }, + { + detail: 0, + format: 4, + mode: 'normal', + style: '', + text: 'strikethrough', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'heading 1', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'heading', + version: 1, + tag: 'h1', + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'heading 2', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'heading', + version: 1, + tag: 'h2', + }, + { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'bullet list ', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'listitem', + version: 1, + value: 1, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'item 2', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'listitem', + version: 1, + value: 2, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'item 3', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'listitem', + version: 1, + value: 3, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'list', + version: 1, + listType: 'bullet', + start: 1, + tag: 'ul', + }, + { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'ordered list', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'listitem', + version: 1, + value: 1, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'item 2', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'listitem', + version: 1, + value: 2, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'item 3', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'listitem', + version: 1, + value: 3, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'list', + version: 1, + listType: 'number', + start: 1, + tag: 'ol', + }, + { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'check list', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'listitem', + version: 1, + value: 1, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'item 2', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'listitem', + version: 1, + value: 2, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'item 3', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'listitem', + version: 1, + value: 3, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'list', + version: 1, + listType: 'check', + start: 1, + tag: 'ul', + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'quoteeee', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'quote', + version: 1, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'code block line ', + type: 'code-highlight', + version: 1, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: '1', + type: 'code-highlight', + version: 1, + highlightType: 'number', + }, + { + type: 'linebreak', + version: 1, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'code block line ', + type: 'code-highlight', + version: 1, + }, + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: '2', + type: 'code-highlight', + version: 1, + highlightType: 'number', + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'code', + version: 1, + language: 'javascript', + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'Upload:', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + { + children: [ + { + type: 'upload', + version: 1, + rawImagePayload: { + value: { + id: '{{UPLOAD_DOC_ID}}', + }, + relationTo: 'uploads', + }, + caption: { + editorState: { + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'upload caption', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'root', + version: 1, + }, + }, + }, + showCaption: true, + data: {}, // populated upload data + }, + ], + direction: null, + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + { + children: [], + direction: null, + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + { + children: [ + { + children: [ + { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: '2x2 table top left', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + ], + direction: null, + format: '', + indent: 0, + type: 'tablecell', + version: 1, + colSpan: 1, + rowSpan: 1, + backgroundColor: null, + headerState: 3, + }, + { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: '2x2 table top right', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + ], + direction: null, + format: '', + indent: 0, + type: 'tablecell', + version: 1, + colSpan: 1, + rowSpan: 1, + backgroundColor: null, + headerState: 1, + }, + ], + direction: null, + format: '', + indent: 0, + type: 'tablerow', + version: 1, + }, + { + children: [ + { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: '2x2 table bottom left', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + ], + direction: null, + format: '', + indent: 0, + type: 'tablecell', + version: 1, + colSpan: 1, + rowSpan: 1, + backgroundColor: null, + headerState: 2, + }, + { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: '2x2 table bottom right', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + ], + direction: null, + format: '', + indent: 0, + type: 'tablecell', + version: 1, + colSpan: 1, + rowSpan: 1, + backgroundColor: null, + headerState: 0, + }, + ], + direction: null, + format: '', + indent: 0, + type: 'tablerow', + version: 1, + }, + ], + direction: null, + format: '', + indent: 0, + type: 'table', + version: 1, + }, + { + rows: [ + { + cells: [ + { + colSpan: 1, + id: 'kafuj', + json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}', + type: 'header', + width: null, + }, + { + colSpan: 1, + id: 'iussu', + json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}', + type: 'header', + width: null, + }, + ], + height: null, + id: 'tnied', + }, + { + cells: [ + { + colSpan: 1, + id: 'hpnnv', + json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}', + type: 'header', + width: null, + }, + { + colSpan: 1, + id: 'ndteg', + json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}', + type: 'normal', + width: null, + }, + ], + height: null, + id: 'rxyey', + }, + { + cells: [ + { + colSpan: 1, + id: 'rtueq', + json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}', + type: 'header', + width: null, + }, + { + colSpan: 1, + id: 'vrzoi', + json: '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}', + type: 'normal', + width: null, + }, + ], + height: null, + id: 'qzglv', + }, + ], + type: 'tablesheet', + version: 1, + }, + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'youtube:', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + { + format: '', + type: 'youtube', + version: 1, + videoID: '3Nwt3qu0_UY', + }, + { + children: [ + { + equation: '3+3', + inline: true, + type: 'equation', + version: 1, + }, + ], + direction: null, + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'collapsible title', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'collapsible-title', + version: 1, + }, + { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: 'collabsible conteent', + type: 'text', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'collapsible-content', + version: 1, + }, + ], + direction: 'ltr', + format: '', + indent: 0, + type: 'collapsible-container', + version: 1, + open: true, + }, + { + children: [], + direction: null, + format: '', + indent: 0, + type: 'paragraph', + version: 1, + }, + { + type: 'horizontalrule', + version: 1, + }, + ], + direction: 'ltr', + }, + }, +} diff --git a/test/fields/collections/LexicalMigrate/index.ts b/test/fields/collections/LexicalMigrate/index.ts new file mode 100644 index 0000000000..262ae976ec --- /dev/null +++ b/test/fields/collections/LexicalMigrate/index.ts @@ -0,0 +1,73 @@ +import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' + +import { + LexicalPluginToLexicalFeature, + LinkFeature, + TreeviewFeature, + UploadFeature, + lexicalEditor, +} from '../../../../packages/richtext-lexical/src' +import { lexicalMigrateFieldsSlug } from '../../slugs' +import { payloadPluginLexicalData } from './generatePayloadPluginLexicalData' + +export const LexicalMigrateFields: CollectionConfig = { + slug: lexicalMigrateFieldsSlug, + admin: { + useAsTitle: 'title', + listSearchableFields: ['title', 'richTextLexicalCustomFields'], + }, + access: { + read: () => true, + }, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + { + name: 'richTextLexicalWithLexicalPluginData', + type: 'richText', + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + LexicalPluginToLexicalFeature(), + TreeviewFeature(), + LinkFeature({ + fields: [ + { + name: 'rel', + label: 'Rel Attribute', + type: 'select', + hasMany: true, + options: ['noopener', 'noreferrer', 'nofollow'], + admin: { + description: + 'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.', + }, + }, + ], + }), + UploadFeature({ + collections: { + uploads: { + fields: [ + { + name: 'caption', + type: 'richText', + editor: lexicalEditor(), + }, + ], + }, + }, + }), + ], + }), + }, + ], +} + +export const LexicalRichTextDoc = { + title: 'Rich Text', + richTextLexicalWithLexicalPluginData: payloadPluginLexicalData, +} diff --git a/test/fields/collections/Number/index.ts b/test/fields/collections/Number/index.ts index 6ceb5b4136..cbe7474eaa 100644 --- a/test/fields/collections/Number/index.ts +++ b/test/fields/collections/Number/index.ts @@ -1,9 +1,11 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { numberFieldsSlug } from '../../slugs' + export const defaultNumber = 5 const NumberFields: CollectionConfig = { - slug: 'number-fields', + slug: numberFieldsSlug, admin: { useAsTitle: 'number', }, diff --git a/test/fields/collections/Point/index.ts b/test/fields/collections/Point/index.ts index 9c274edac2..8ebece236a 100644 --- a/test/fields/collections/Point/index.ts +++ b/test/fields/collections/Point/index.ts @@ -1,6 +1,6 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' -export const pointFieldsSlug = 'point-fields' +import { pointFieldsSlug } from '../../slugs' const PointFields: CollectionConfig = { slug: pointFieldsSlug, diff --git a/test/fields/collections/Radio/index.ts b/test/fields/collections/Radio/index.ts index 199a847fdf..323b983669 100644 --- a/test/fields/collections/Radio/index.ts +++ b/test/fields/collections/Radio/index.ts @@ -1,7 +1,9 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { radioFieldsSlug } from '../../slugs' + const RadioFields: CollectionConfig = { - slug: 'radio-fields', + slug: radioFieldsSlug, fields: [ { name: 'radio', diff --git a/test/fields/collections/Relationship/index.ts b/test/fields/collections/Relationship/index.ts index 157cab3f9c..cda5f549e4 100644 --- a/test/fields/collections/Relationship/index.ts +++ b/test/fields/collections/Relationship/index.ts @@ -1,6 +1,6 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' -export const relationshipFieldsSlug = 'relationship-fields' +import { relationshipFieldsSlug } from '../../slugs' const RelationshipFields: CollectionConfig = { fields: [ diff --git a/test/fields/collections/RichText/data.ts b/test/fields/collections/RichText/data.ts new file mode 100644 index 0000000000..03872b8f44 --- /dev/null +++ b/test/fields/collections/RichText/data.ts @@ -0,0 +1,135 @@ +import { generateLexicalRichText } from './generateLexicalRichText' +import { generateSlateRichText } from './generateSlateRichText' + +export const richTextBlocks = [ + { + blockType: 'textBlock', + text: 'Regular text', + }, + { + blockType: 'richTextBlock', + text: [ + { + children: [ + { + text: 'Rich text', + }, + ], + type: 'h1', + }, + ], + }, +] +export const richTextDoc = { + title: 'Rich Text', + selectHasMany: ['one', 'five'], + richText: generateSlateRichText(), + richTextReadOnly: generateSlateRichText(), + richTextCustomFields: generateSlateRichText(), + richTextLexicalCustomFields: generateLexicalRichText(), + blocks: richTextBlocks, +} + +export const richTextBulletsDoc = { + title: 'Bullets and Indentation', + richTextLexicalCustomFields: generateLexicalRichText(), + richText: [ + { + type: 'ul', + children: [ + { + type: 'li', + children: [ + { + children: [ + { + text: 'I am semantically connected to my sub-bullets', + }, + ], + }, + { + type: 'ul', + children: [ + { + type: 'li', + children: [ + { + text: 'I am sub-bullets that are semantically connected to the parent bullet', + }, + ], + }, + ], + }, + ], + }, + { + children: [ + { + text: 'Normal bullet', + }, + ], + type: 'li', + }, + { + type: 'li', + children: [ + { + type: 'ul', + children: [ + { + type: 'li', + children: [ + { + text: 'I am the old style of sub-bullet', + }, + ], + }, + ], + }, + ], + }, + { + type: 'li', + children: [ + { + text: 'Another normal bullet', + }, + ], + }, + { + type: 'li', + children: [ + { + children: [ + { + text: 'This text precedes a nested list', + }, + ], + }, + { + type: 'ul', + children: [ + { + type: 'li', + children: [ + { + text: 'I am a sub-bullet', + }, + ], + }, + { + type: 'li', + children: [ + { + text: 'And I am another sub-bullet', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], +} diff --git a/test/fields/collections/RichText/index.ts b/test/fields/collections/RichText/index.ts index 86cc7c9e7c..d6cc89db9d 100644 --- a/test/fields/collections/RichText/index.ts +++ b/test/fields/collections/RichText/index.ts @@ -10,12 +10,13 @@ import { } from '../../../../packages/richtext-lexical/src' import { lexicalHTML } from '../../../../packages/richtext-lexical/src/field/features/converters/html/field' import { slateEditor } from '../../../../packages/richtext-slate/src' +import { richTextFieldsSlug } from '../../slugs' import { RelationshipBlock, SelectFieldBlock, TextBlock, UploadAndRichTextBlock } from './blocks' import { generateLexicalRichText } from './generateLexicalRichText' import { generateSlateRichText } from './generateSlateRichText' const RichTextFields: CollectionConfig = { - slug: 'rich-text-fields', + slug: richTextFieldsSlug, admin: { useAsTitle: 'title', }, @@ -317,137 +318,4 @@ const RichTextFields: CollectionConfig = { ], } -export const richTextBulletsDoc = { - title: 'Bullets and Indentation', - richTextLexicalCustomFields: generateLexicalRichText(), - richText: [ - { - type: 'ul', - children: [ - { - type: 'li', - children: [ - { - children: [ - { - text: 'I am semantically connected to my sub-bullets', - }, - ], - }, - { - type: 'ul', - children: [ - { - type: 'li', - children: [ - { - text: 'I am sub-bullets that are semantically connected to the parent bullet', - }, - ], - }, - ], - }, - ], - }, - { - children: [ - { - text: 'Normal bullet', - }, - ], - type: 'li', - }, - { - type: 'li', - children: [ - { - type: 'ul', - children: [ - { - type: 'li', - children: [ - { - text: 'I am the old style of sub-bullet', - }, - ], - }, - ], - }, - ], - }, - { - type: 'li', - children: [ - { - text: 'Another normal bullet', - }, - ], - }, - { - type: 'li', - children: [ - { - children: [ - { - text: 'This text precedes a nested list', - }, - ], - }, - { - type: 'ul', - children: [ - { - type: 'li', - children: [ - { - text: 'I am a sub-bullet', - }, - ], - }, - { - type: 'li', - children: [ - { - text: 'And I am another sub-bullet', - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], -} - -export const richTextBlocks = [ - { - blockType: 'textBlock', - text: 'Regular text', - }, - { - blockType: 'richTextBlock', - text: [ - { - children: [ - { - text: 'Rich text', - }, - ], - type: 'h1', - }, - ], - }, -] -export const richTextDoc = { - title: 'Rich Text', - selectHasMany: ['one', 'five'], - richText: generateSlateRichText(), - richTextReadOnly: generateSlateRichText(), - richTextCustomFields: generateSlateRichText(), - richTextLexicalCustomFields: generateLexicalRichText(), - blocks: richTextBlocks, -} - export default RichTextFields diff --git a/test/fields/collections/Row/index.ts b/test/fields/collections/Row/index.ts index f0cdb3460f..631db4cb2f 100644 --- a/test/fields/collections/Row/index.ts +++ b/test/fields/collections/Row/index.ts @@ -1,6 +1,6 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' -export const rowFieldsSlug = 'row-fields' +import { rowFieldsSlug } from '../../slugs' const RowFields: CollectionConfig = { slug: rowFieldsSlug, diff --git a/test/fields/collections/Select/index.ts b/test/fields/collections/Select/index.ts index 02abff6f4a..4171997be4 100644 --- a/test/fields/collections/Select/index.ts +++ b/test/fields/collections/Select/index.ts @@ -1,7 +1,9 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { selectFieldsSlug } from '../../slugs' + const SelectFields: CollectionConfig = { - slug: 'select-fields', + slug: selectFieldsSlug, fields: [ { name: 'select', diff --git a/test/fields/collections/Tabs/constants.ts b/test/fields/collections/Tabs/constants.ts index f03dde1749..182d51a4a2 100644 --- a/test/fields/collections/Tabs/constants.ts +++ b/test/fields/collections/Tabs/constants.ts @@ -1,5 +1,3 @@ -export const tabsSlug = 'tabs-fields' - export const namedTabText = 'Some text in a named tab' export const namedTabDefaultValue = 'default text inside of a named tab' export const localizedTextValue = 'localized text' diff --git a/test/fields/collections/Tabs/index.ts b/test/fields/collections/Tabs/index.ts index 6c86ade01a..10f8ec607c 100644 --- a/test/fields/collections/Tabs/index.ts +++ b/test/fields/collections/Tabs/index.ts @@ -1,12 +1,13 @@ /* eslint-disable no-param-reassign */ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { tabsFieldsSlug } from '../../slugs' import { getBlocksField, getBlocksFieldSeedData } from '../Blocks' import { UIField } from './UIField' -import { localizedTextValue, namedTabDefaultValue, namedTabText, tabsSlug } from './constants' +import { localizedTextValue, namedTabDefaultValue, namedTabText } from './constants' const TabsFields: CollectionConfig = { - slug: tabsSlug, + slug: tabsFieldsSlug, access: { read: () => true, }, diff --git a/test/fields/collections/Text/index.ts b/test/fields/collections/Text/index.ts index 33007c69f6..ec190826da 100644 --- a/test/fields/collections/Text/index.ts +++ b/test/fields/collections/Text/index.ts @@ -1,7 +1,8 @@ import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { textFieldsSlug } from '../../slugs' + export const defaultText = 'default-text' -export const textFieldsSlug = 'text-fields' const TextFields: CollectionConfig = { slug: textFieldsSlug, diff --git a/test/fields/collections/Upload/index.ts b/test/fields/collections/Upload/index.ts index da72adcf6a..ff48212080 100644 --- a/test/fields/collections/Upload/index.ts +++ b/test/fields/collections/Upload/index.ts @@ -2,8 +2,10 @@ import path from 'path' import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { uploadsSlug } from '../../slugs' + const Uploads: CollectionConfig = { - slug: 'uploads', + slug: uploadsSlug, upload: { staticDir: path.resolve(__dirname, './uploads'), }, @@ -15,7 +17,7 @@ const Uploads: CollectionConfig = { { type: 'upload', name: 'media', - relationTo: 'uploads', + relationTo: uploadsSlug, filterOptions: { mimeType: { equals: 'image/png', diff --git a/test/fields/collections/Upload2/index.ts b/test/fields/collections/Upload2/index.ts index 3f73829cd3..e10ca95bfe 100644 --- a/test/fields/collections/Upload2/index.ts +++ b/test/fields/collections/Upload2/index.ts @@ -2,8 +2,10 @@ import path from 'path' import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { uploads2Slug } from '../../slugs' + const Uploads2: CollectionConfig = { - slug: 'uploads2', + slug: uploads2Slug, upload: { staticDir: path.resolve(__dirname, './uploads2'), }, @@ -19,7 +21,7 @@ const Uploads2: CollectionConfig = { { type: 'upload', name: 'media', - relationTo: 'uploads2', + relationTo: uploads2Slug, }, ], } diff --git a/test/fields/collections/Uploads3/index.ts b/test/fields/collections/Uploads3/index.ts index 75d7a4bb94..5e080331d4 100644 --- a/test/fields/collections/Uploads3/index.ts +++ b/test/fields/collections/Uploads3/index.ts @@ -2,8 +2,10 @@ import path from 'path' import type { CollectionConfig } from '../../../../packages/payload/src/collections/config/types' +import { uploads3Slug } from '../../slugs' + const Uploads3: CollectionConfig = { - slug: 'uploads3', + slug: uploads3Slug, upload: { staticDir: path.resolve(__dirname, './uploads3'), }, @@ -18,7 +20,7 @@ const Uploads3: CollectionConfig = { { type: 'upload', name: 'media', - relationTo: 'uploads3', + relationTo: uploads3Slug, }, { type: 'richText', diff --git a/test/fields/config.ts b/test/fields/config.ts index 4b21cbe72f..bd5946d48a 100644 --- a/test/fields/config.ts +++ b/test/fields/config.ts @@ -1,33 +1,75 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import fs from 'fs' import path from 'path' -import getFileByPath from '../../packages/payload/src/uploads/getFileByPath' +import type { CollectionConfig } from '../../packages/payload/src/collections/config/types' + import { buildConfigWithDefaults } from '../buildConfigWithDefaults' -import { devUser } from '../credentials' -import ArrayFields, { arrayDoc } from './collections/Array' -import BlockFields, { blocksDoc } from './collections/Blocks' +import ArrayFields from './collections/Array' +import BlockFields from './collections/Blocks' import CheckboxFields from './collections/Checkbox' -import CodeFields, { codeDoc } from './collections/Code' -import CollapsibleFields, { collapsibleDoc } from './collections/Collapsible' -import ConditionalLogic, { conditionalLogicDoc } from './collections/ConditionalLogic' -import DateFields, { dateDoc } from './collections/Date' -import GroupFields, { groupDoc } from './collections/Group' +import CodeFields from './collections/Code' +import CollapsibleFields from './collections/Collapsible' +import ConditionalLogic from './collections/ConditionalLogic' +import DateFields from './collections/Date' +import GroupFields from './collections/Group' import IndexedFields from './collections/Indexed' -import JSONFields, { jsonDoc } from './collections/JSON' -import { LexicalFields, LexicalRichTextDoc } from './collections/Lexical' -import NumberFields, { numberDoc } from './collections/Number' -import PointFields, { pointDoc } from './collections/Point' -import RadioFields, { radiosDoc } from './collections/Radio' +import JSONFields from './collections/JSON' +import { LexicalFields } from './collections/Lexical' +import { LexicalMigrateFields } from './collections/LexicalMigrate' +import NumberFields from './collections/Number' +import PointFields from './collections/Point' +import RadioFields from './collections/Radio' import RelationshipFields from './collections/Relationship' -import RichTextFields, { richTextBulletsDoc, richTextDoc } from './collections/RichText' +import RichTextFields from './collections/RichText' import RowFields from './collections/Row' -import SelectFields, { selectsDoc } from './collections/Select' -import TabsFields, { tabsDoc } from './collections/Tabs' -import TextFields, { textDoc, textFieldsSlug } from './collections/Text' -import Uploads, { uploadsDoc } from './collections/Upload' +import SelectFields from './collections/Select' +import TabsFields from './collections/Tabs' +import TextFields from './collections/Text' +import Uploads from './collections/Upload' import Uploads2 from './collections/Upload2' import Uploads3 from './collections/Uploads3' +import { clearAndSeedEverything } from './seed' + +export const collectionSlugs: CollectionConfig[] = [ + LexicalFields, + LexicalMigrateFields, + { + admin: { + useAsTitle: 'email', + }, + auth: true, + fields: [ + { + name: 'canViewConditionalField', + defaultValue: true, + type: 'checkbox', + }, + ], + slug: 'users', + }, + ArrayFields, + BlockFields, + CheckboxFields, + CodeFields, + CollapsibleFields, + ConditionalLogic, + DateFields, + RadioFields, + GroupFields, + RowFields, + IndexedFields, + JSONFields, + NumberFields, + PointFields, + RelationshipFields, + RichTextFields, + SelectFields, + TabsFields, + TextFields, + Uploads, + Uploads2, + Uploads3, +] export default buildConfigWithDefaults({ admin: { @@ -42,139 +84,13 @@ export default buildConfigWithDefaults({ }, }), }, - collections: [ - LexicalFields, - { - admin: { - useAsTitle: 'email', - }, - auth: true, - fields: [ - { - name: 'canViewConditionalField', - defaultValue: true, - type: 'checkbox', - }, - ], - slug: 'users', - }, - ArrayFields, - BlockFields, - CheckboxFields, - CodeFields, - CollapsibleFields, - ConditionalLogic, - DateFields, - RadioFields, - GroupFields, - RowFields, - IndexedFields, - JSONFields, - NumberFields, - PointFields, - RelationshipFields, - RichTextFields, - SelectFields, - TabsFields, - TextFields, - Uploads, - Uploads2, - Uploads3, - ], + collections: collectionSlugs, localization: { defaultLocale: 'en', fallback: true, locales: ['en', 'es'], }, onInit: async (payload) => { - await payload.create({ - collection: 'users', - data: { - email: devUser.email, - password: devUser.password, - }, - }) - - const createdArrayDoc = await payload.create({ collection: 'array-fields', data: arrayDoc }) - await payload.create({ collection: 'collapsible-fields', data: collapsibleDoc }) - await payload.create({ collection: 'conditional-logic', data: conditionalLogicDoc }) - await payload.create({ collection: 'group-fields', data: groupDoc }) - await payload.create({ collection: 'select-fields', data: selectsDoc }) - await payload.create({ collection: 'radio-fields', data: radiosDoc }) - await payload.create({ collection: 'tabs-fields', data: tabsDoc }) - await payload.create({ collection: 'point-fields', data: pointDoc }) - await payload.create({ collection: 'date-fields', data: dateDoc }) - await payload.create({ collection: 'code-fields', data: codeDoc }) - await payload.create({ collection: 'json-fields', data: jsonDoc }) - - const createdTextDoc = await payload.create({ collection: textFieldsSlug, data: textDoc }) - - const uploadsDir = path.resolve(__dirname, './collections/Upload/uploads') - - if (fs.existsSync(uploadsDir)) - fs.readdirSync(uploadsDir).forEach((f) => fs.rmSync(`${uploadsDir}/${f}`)) - - const pngPath = path.resolve(__dirname, './uploads/payload.png') - const pngFile = await getFileByPath(pngPath) - const createdPNGDoc = await payload.create({ collection: 'uploads', data: {}, file: pngFile }) - - const jpgPath = path.resolve(__dirname, './collections/Upload/payload.jpg') - const jpgFile = await getFileByPath(jpgPath) - const createdJPGDoc = await payload.create({ - collection: 'uploads', - data: { - ...uploadsDoc, - media: createdPNGDoc.id, - }, - file: jpgFile, - }) - - const formattedID = - payload.db.defaultIDType === 'number' ? createdArrayDoc.id : `"${createdArrayDoc.id}"` - - const formattedJPGID = - payload.db.defaultIDType === 'number' ? createdJPGDoc.id : `"${createdJPGDoc.id}"` - - const formattedTextID = - payload.db.defaultIDType === 'number' ? createdTextDoc.id : `"${createdTextDoc.id}"` - - const richTextDocWithRelId = JSON.parse( - JSON.stringify(richTextDoc) - .replace(/"\{\{ARRAY_DOC_ID\}\}"/g, formattedID) - .replace(/"\{\{UPLOAD_DOC_ID\}\}"/g, formattedJPGID) - .replace(/"\{\{TEXT_DOC_ID\}\}"/g, formattedTextID), - ) - const richTextBulletsDocWithRelId = JSON.parse( - JSON.stringify(richTextBulletsDoc) - .replace(/"\{\{ARRAY_DOC_ID\}\}"/g, formattedID) - .replace(/"\{\{UPLOAD_DOC_ID\}\}"/g, formattedJPGID) - .replace(/"\{\{TEXT_DOC_ID\}\}"/g, formattedTextID), - ) - - const lexicalRichTextDocWithRelId = JSON.parse( - JSON.stringify(LexicalRichTextDoc) - .replace(/"\{\{ARRAY_DOC_ID\}\}"/g, formattedID) - .replace(/"\{\{UPLOAD_DOC_ID\}\}"/g, formattedJPGID) - .replace(/"\{\{TEXT_DOC_ID\}\}"/g, formattedTextID), - ) - await payload.create({ collection: 'lexical-fields', data: lexicalRichTextDocWithRelId }) - - const richTextDocWithRelationship = { ...richTextDocWithRelId } - - await payload.create({ collection: 'rich-text-fields', data: richTextBulletsDocWithRelId }) - await payload.create({ collection: 'rich-text-fields', data: richTextDocWithRelationship }) - - await payload.create({ collection: 'number-fields', data: { number: 2 } }) - await payload.create({ collection: 'number-fields', data: { number: 3 } }) - await payload.create({ collection: 'number-fields', data: numberDoc }) - - const blocksDocWithRichText = { ...blocksDoc } - - // @ts-ignore - blocksDocWithRichText.blocks[0].richText = richTextDocWithRelationship.richText - // @ts-ignore - blocksDocWithRichText.localizedBlocks[0].richText = richTextDocWithRelationship.richText - - await payload.create({ collection: 'block-fields', data: blocksDocWithRichText }) + await clearAndSeedEverything(payload) }, }) diff --git a/test/fields/e2e.spec.ts b/test/fields/e2e.spec.ts index 89c031eece..3473d13dd1 100644 --- a/test/fields/e2e.spec.ts +++ b/test/fields/e2e.spec.ts @@ -10,19 +10,25 @@ import { saveDocAndAssert, saveDocHotkeyAndAssert } from '../helpers' import { AdminUrlUtil } from '../helpers/adminUrlUtil' import { initPayloadE2E } from '../helpers/configHelpers' import { RESTClient } from '../helpers/rest' -import { collapsibleFieldsSlug } from './collections/Collapsible/shared' import { jsonDoc } from './collections/JSON' import { numberDoc } from './collections/Number' -import { pointFieldsSlug } from './collections/Point' -import { relationshipFieldsSlug } from './collections/Relationship' -import { tabsSlug } from './collections/Tabs/constants' -import { textDoc, textFieldsSlug } from './collections/Text' +import { textDoc } from './collections/Text' +import { lexicalE2E } from './lexicalE2E' +import { clearAndSeedEverything } from './seed' +import { + collapsibleFieldsSlug, + pointFieldsSlug, + relationshipFieldsSlug, + tabsFieldsSlug, + textFieldsSlug, +} from './slugs' -const { afterEach, beforeAll, describe } = test +const { afterEach, beforeAll, describe, beforeEach } = test let client: RESTClient let page: Page -let serverURL +let serverURL: string +// If we want to make this run in parallel: test.describe.configure({ mode: 'parallel' }) describe('fields', () => { beforeAll(async ({ browser }) => { @@ -34,7 +40,12 @@ describe('fields', () => { const context = await browser.newContext() page = await context.newPage() }) - + beforeEach(async () => { + await clearAndSeedEverything(payload) + await client.logout() + client = new RESTClient(null, { serverURL, defaultSlug: 'users' }) + await client.login() + }) describe('text', () => { let url: AdminUrlUtil beforeAll(() => { @@ -146,7 +157,7 @@ describe('fields', () => { describe('indexed', () => { let url: AdminUrlUtil - beforeAll(() => { + beforeEach(() => { url = new AdminUrlUtil(serverURL, 'indexed-fields') }) @@ -162,13 +173,14 @@ describe('fields', () => { }, }, }) + await page.goto(url.create) await page.locator('#field-text').fill('test') await page.locator('#field-uniqueText').fill(uniqueText) // attempt to save - await page.locator('#action-save').click() + await page.click('#action-save', { delay: 100 }) // toast error await expect(page.locator('.Toastify')).toContainText( @@ -269,7 +281,7 @@ describe('fields', () => { let url: AdminUrlUtil let filledGroupPoint let emptyGroupPoint - beforeAll(async () => { + beforeEach(async () => { url = new AdminUrlUtil(serverURL, pointFieldsSlug) filledGroupPoint = await payload.create({ collection: pointFieldsSlug, @@ -682,7 +694,7 @@ describe('fields', () => { describe('tabs', () => { let url: AdminUrlUtil beforeAll(() => { - url = new AdminUrlUtil(serverURL, tabsSlug) + url = new AdminUrlUtil(serverURL, tabsFieldsSlug) }) test('should fill and retain a new value within a tab while switching tabs', async () => { @@ -750,7 +762,7 @@ describe('fields', () => { ) }) }) - + describe('lexical', lexicalE2E(client, page, serverURL)) describe('richText', () => { async function navigateToRichTextFields() { const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields') @@ -1473,7 +1485,7 @@ describe('fields', () => { url = new AdminUrlUtil(serverURL, 'uploads') }) - test('should upload files', async () => { + async function uploadImage() { await page.goto(url.create) // create a jpg upload @@ -1484,10 +1496,15 @@ describe('fields', () => { await page.locator('#action-save').click() await wait(200) await expect(page.locator('.Toastify')).toContainText('successfully') + } + + test('should upload files', async () => { + await uploadImage() }) // test that the image renders test('should render uploaded image', async () => { + await uploadImage() await expect(page.locator('.file-field .file-details img')).toHaveAttribute( 'src', '/uploads/payload-1.jpg', @@ -1495,6 +1512,7 @@ describe('fields', () => { }) test('should upload using the document drawer', async () => { + await uploadImage() // Open the media drawer and create a png upload await page.locator('.field-type.upload .upload__toggler.doc-drawer__toggler').click() await page @@ -1521,10 +1539,20 @@ describe('fields', () => { }) test('should clear selected upload', async () => { + await uploadImage() + await page.locator('.field-type.upload .upload__toggler.doc-drawer__toggler').click() + await page + .locator('[id^=doc-drawer_uploads_1_] .file-field__upload input[type="file"]') + .setInputFiles(path.resolve(__dirname, './uploads/payload.png')) + await page.locator('[id^=doc-drawer_uploads_1_] #action-save').click() + await wait(200) + await expect(page.locator('.Toastify')).toContainText('successfully') await page.locator('.field-type.upload .file-details__remove').click() }) test('should select using the list drawer and restrict mimetype based on filterOptions', async () => { + await uploadImage() + await page.locator('.field-type.upload .upload__toggler.list-drawer__toggler').click() await wait(200) const jpgImages = page.locator('[id^=list-drawer_1_] .upload-gallery img[src$=".jpg"]') diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 81518a326d..69f604ee5c 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -8,28 +8,24 @@ import type { RichTextField } from './payload-types' import payload from '../../packages/payload/src' import { initPayloadTest } from '../helpers/configHelpers' +import { isMongoose } from '../helpers/isMongoose' import { RESTClient } from '../helpers/rest' import configPromise from '../uploads/config' -import { arrayDefaultValue, arrayFieldsSlug } from './collections/Array' +import { arrayDefaultValue } from './collections/Array' import { blocksDoc } from './collections/Blocks' import { dateDoc } from './collections/Date' -import { - groupDefaultChild, - groupDefaultValue, - groupDoc, - groupFieldsSlug, -} from './collections/Group' +import { groupDefaultChild, groupDefaultValue, groupDoc } from './collections/Group' import { defaultNumber, numberDoc } from './collections/Number' import { pointDoc } from './collections/Point' -import { relationshipFieldsSlug } from './collections/Relationship' import { tabsDoc } from './collections/Tabs' import { localizedTextValue, namedTabDefaultValue, namedTabText, - tabsSlug, } from './collections/Tabs/constants' import { defaultText } from './collections/Text' +import { clearAndSeedEverything } from './seed' +import { arrayFieldsSlug, groupFieldsSlug, relationshipFieldsSlug, tabsFieldsSlug } from './slugs' let client let graphQLClient: GraphQLClient @@ -48,10 +44,16 @@ describe('Fields', () => { token = await client.login() }) + beforeEach(async () => { + await clearAndSeedEverything(payload) + client = new RESTClient(config, { defaultSlug: 'point-fields', serverURL }) + await client.login() + }) + describe('text', () => { let doc const text = 'text field' - beforeAll(async () => { + beforeEach(async () => { doc = await payload.create({ collection: 'text-fields', data: { text }, @@ -86,7 +88,7 @@ describe('Fields', () => { const otherTextDocText = 'alt text' const relationshipText = 'relationship text' - beforeAll(async () => { + beforeEach(async () => { textDoc = await payload.create({ collection: 'text-fields', data: { @@ -193,7 +195,7 @@ describe('Fields', () => { describe('timestamps', () => { const tenMinutesAgo = new Date(Date.now() - 1000 * 60 * 10) let doc - beforeAll(async () => { + beforeEach(async () => { doc = await payload.create({ collection: 'date-fields', data: dateDoc, @@ -231,7 +233,7 @@ describe('Fields', () => { describe('select', () => { let doc - beforeAll(async () => { + beforeEach(async () => { const { id } = await payload.create({ collection: 'select-fields', data: { @@ -273,7 +275,7 @@ describe('Fields', () => { describe('number', () => { let doc - beforeAll(async () => { + beforeEach(async () => { doc = await payload.create({ collection: 'number-fields', data: numberDoc, @@ -375,13 +377,13 @@ describe('Fields', () => { }) }) - if (['mongoose'].includes(process.env.PAYLOAD_DATABASE)) { + if (isMongoose(payload) || !['postgres'].includes(process.env.PAYLOAD_DATABASE)) { describe('indexes', () => { let indexes const definitions: Record = {} const options: Record = {} - beforeAll(() => { + beforeEach(() => { indexes = (payload.db as MongooseAdapter).collections[ 'indexed-fields' ].schema.indexes() as [Record, IndexOptions] @@ -434,7 +436,7 @@ describe('Fields', () => { const definitions: Record = {} const options: Record = {} - beforeAll(() => { + beforeEach(() => { indexes = (payload.db as MongooseAdapter).versions['indexed-fields'].schema.indexes() as [ Record, IndexOptions, @@ -458,7 +460,7 @@ describe('Fields', () => { const localized = [5, -2] const group = { point: [1, 9] } - beforeAll(async () => { + beforeEach(async () => { const findDoc = await payload.find({ collection: 'point-fields', pagination: false, @@ -495,6 +497,17 @@ describe('Fields', () => { }) it('should not create duplicate point when unique', async () => { + // first create the point field + doc = await payload.create({ + collection: 'point-fields', + data: { + group, + localized, + point, + }, + }) + + // Now make sure we can't create a duplicate (since 'localized' is a unique field) await expect(() => payload.create({ collection: 'point-fields', @@ -546,7 +559,7 @@ describe('Fields', () => { let doc const collection = arrayFieldsSlug - beforeAll(async () => { + beforeEach(async () => { doc = await payload.create({ collection, data: {}, @@ -611,7 +624,7 @@ describe('Fields', () => { describe('group', () => { let document - beforeAll(async () => { + beforeEach(async () => { document = await payload.create({ collection: groupFieldsSlug, data: {}, @@ -627,9 +640,9 @@ describe('Fields', () => { describe('tabs', () => { let document - beforeAll(async () => { + beforeEach(async () => { document = await payload.create({ - collection: tabsSlug, + collection: tabsFieldsSlug, data: tabsDoc, }) }) @@ -649,7 +662,7 @@ describe('Fields', () => { it('should create with localized text inside a named tab', async () => { document = await payload.findByID({ id: document.id, - collection: tabsSlug, + collection: tabsFieldsSlug, locale: 'all', }) expect(document.localizedTab.en.text).toStrictEqual(localizedTextValue) @@ -658,7 +671,7 @@ describe('Fields', () => { it('should allow access control on a named tab', async () => { document = await payload.findByID({ id: document.id, - collection: tabsSlug, + collection: tabsFieldsSlug, overrideAccess: false, }) expect(document.accessControlTab).toBeUndefined() @@ -666,7 +679,7 @@ describe('Fields', () => { it('should allow hooks on a named tab', async () => { const newDocument = await payload.create({ - collection: tabsSlug, + collection: tabsFieldsSlug, data: tabsDoc, }) expect(newDocument.hooksTab.beforeValidate).toBe(true) diff --git a/test/fields/lexicalE2E.ts b/test/fields/lexicalE2E.ts new file mode 100644 index 0000000000..a8d69a5cd5 --- /dev/null +++ b/test/fields/lexicalE2E.ts @@ -0,0 +1,27 @@ +import type { Page } from '@playwright/test' + +import { expect, test } from '@playwright/test' + +import type { RESTClient } from '../helpers/rest' + +import { AdminUrlUtil } from '../helpers/adminUrlUtil' + +const { describe } = test + +export const lexicalE2E = (client: RESTClient, page: Page, serverURL: string) => { + async function navigateToRichTextFields() { + const url: AdminUrlUtil = new AdminUrlUtil(serverURL, 'rich-text-fields') + await page.goto(url.list) + await page.locator('.row-1 .cell-title a').click() + } + + return () => { + describe('todo', () => { + test.skip('todo', async () => { + await navigateToRichTextFields() + + await page.locator('todo').first().click() + }) + }) + } +} diff --git a/test/fields/seed.ts b/test/fields/seed.ts new file mode 100644 index 0000000000..97642cb493 --- /dev/null +++ b/test/fields/seed.ts @@ -0,0 +1,147 @@ +import path from 'path' + +import { type Payload } from '../../packages/payload/src' +import getFileByPath from '../../packages/payload/src/uploads/getFileByPath' +import { devUser } from '../credentials' +import { seedDB } from '../helpers/seed' +import { arrayDoc } from './collections/Array' +import { blocksDoc } from './collections/Blocks' +import { codeDoc } from './collections/Code' +import { collapsibleDoc } from './collections/Collapsible' +import { conditionalLogicDoc } from './collections/ConditionalLogic' +import { dateDoc } from './collections/Date' +import { groupDoc } from './collections/Group' +import { jsonDoc } from './collections/JSON' +import { lexicalRichTextDoc } from './collections/Lexical/data' +import { numberDoc } from './collections/Number' +import { pointDoc } from './collections/Point' +import { radiosDoc } from './collections/Radio' +import { richTextBulletsDoc, richTextDoc } from './collections/RichText/data' +import { selectsDoc } from './collections/Select' +import { tabsDoc } from './collections/Tabs' +import { textDoc } from './collections/Text' +import { uploadsDoc } from './collections/Upload' +import { + blockFieldsSlug, + codeFieldsSlug, + collapsibleFieldsSlug, + collectionSlugs, + conditionalLogicSlug, + dateFieldsSlug, + groupFieldsSlug, + jsonFieldsSlug, + lexicalFieldsSlug, + lexicalMigrateFieldsSlug, + numberFieldsSlug, + pointFieldsSlug, + radioFieldsSlug, + richTextFieldsSlug, + selectFieldsSlug, + tabsFieldsSlug, + textFieldsSlug, + uploadsSlug, + usersSlug, +} from './slugs' + +export async function clearAndSeedEverything(_payload: Payload) { + return await seedDB({ + snapshotKey: 'fieldsTest', + shouldResetDB: true, + collectionSlugs, + _payload, + uploadsDir: path.resolve(__dirname, './collections/Upload/uploads'), + seedFunction: async (_payload) => { + const jpgPath = path.resolve(__dirname, './collections/Upload/payload.jpg') + const pngPath = path.resolve(__dirname, './uploads/payload.png') + + // Get both files in parallel + const [jpgFile, pngFile] = await Promise.all([getFileByPath(jpgPath), getFileByPath(pngPath)]) + + const [createdArrayDoc, createdTextDoc, createdPNGDoc] = await Promise.all([ + _payload.create({ collection: 'array-fields', data: arrayDoc }), + _payload.create({ collection: textFieldsSlug, data: textDoc }), + _payload.create({ collection: uploadsSlug, data: {}, file: pngFile }), + ]) + + const createdJPGDoc = await _payload.create({ + collection: uploadsSlug, + data: { + ...uploadsDoc, + media: createdPNGDoc.id, + }, + file: jpgFile, + }) + + const formattedID = + _payload.db.defaultIDType === 'number' ? createdArrayDoc.id : `"${createdArrayDoc.id}"` + + const formattedJPGID = + _payload.db.defaultIDType === 'number' ? createdJPGDoc.id : `"${createdJPGDoc.id}"` + + const formattedTextID = + _payload.db.defaultIDType === 'number' ? createdTextDoc.id : `"${createdTextDoc.id}"` + + const richTextDocWithRelId = JSON.parse( + JSON.stringify(richTextDoc) + .replace(/"\{\{ARRAY_DOC_ID\}\}"/g, `${formattedID}`) + .replace(/"\{\{UPLOAD_DOC_ID\}\}"/g, `${formattedJPGID}`) + .replace(/"\{\{TEXT_DOC_ID\}\}"/g, `${formattedTextID}`), + ) + const richTextBulletsDocWithRelId = JSON.parse( + JSON.stringify(richTextBulletsDoc) + .replace(/"\{\{ARRAY_DOC_ID\}\}"/g, `${formattedID}`) + .replace(/"\{\{UPLOAD_DOC_ID\}\}"/g, `${formattedJPGID}`) + .replace(/"\{\{TEXT_DOC_ID\}\}"/g, `${formattedTextID}`), + ) + + const richTextDocWithRelationship = { ...richTextDocWithRelId } + + const blocksDocWithRichText = { ...blocksDoc } + + blocksDocWithRichText.blocks[0].richText = richTextDocWithRelationship.richText + blocksDocWithRichText.localizedBlocks[0].richText = richTextDocWithRelationship.richText + + const lexicalRichTextDocWithRelId = JSON.parse( + JSON.stringify(lexicalRichTextDoc) + .replace(/"\{\{ARRAY_DOC_ID\}\}"/g, `${formattedID}`) + .replace(/"\{\{UPLOAD_DOC_ID\}\}"/g, `${formattedJPGID}`) + .replace(/"\{\{TEXT_DOC_ID\}\}"/g, `${formattedTextID}`), + ) + + await Promise.all([ + _payload.create({ + collection: usersSlug, + data: { + email: devUser.email, + password: devUser.password, + }, + }), + _payload.create({ collection: collapsibleFieldsSlug, data: collapsibleDoc }), + _payload.create({ collection: conditionalLogicSlug, data: conditionalLogicDoc }), + _payload.create({ collection: groupFieldsSlug, data: groupDoc }), + _payload.create({ collection: selectFieldsSlug, data: selectsDoc }), + _payload.create({ collection: radioFieldsSlug, data: radiosDoc }), + _payload.create({ collection: tabsFieldsSlug, data: tabsDoc }), + _payload.create({ collection: pointFieldsSlug, data: pointDoc }), + _payload.create({ collection: dateFieldsSlug, data: dateDoc }), + _payload.create({ collection: codeFieldsSlug, data: codeDoc }), + _payload.create({ collection: jsonFieldsSlug, data: jsonDoc }), + + _payload.create({ collection: blockFieldsSlug, data: blocksDocWithRichText }), + + _payload.create({ collection: lexicalFieldsSlug, data: lexicalRichTextDocWithRelId }), + _payload.create({ + collection: lexicalMigrateFieldsSlug, + data: lexicalRichTextDocWithRelId, + }), + + _payload.create({ collection: richTextFieldsSlug, data: richTextBulletsDocWithRelId }), + _payload.create({ collection: richTextFieldsSlug, data: richTextDocWithRelationship }), + + _payload.create({ collection: numberFieldsSlug, data: { number: 2 } }), + _payload.create({ collection: numberFieldsSlug, data: { number: 3 } }), + _payload.create({ collection: numberFieldsSlug, data: numberDoc }), + ]) + }, + }) +} diff --git a/test/fields/slugs.ts b/test/fields/slugs.ts new file mode 100644 index 0000000000..3b5a074263 --- /dev/null +++ b/test/fields/slugs.ts @@ -0,0 +1,52 @@ +export const usersSlug = 'users' as const +export const arrayFieldsSlug = 'array-fields' as const +export const blockFieldsSlug = 'block-fields' as const +export const checkboxFieldsSlug = 'checkbox-fields' as const +export const codeFieldsSlug = 'code-fields' as const +export const collapsibleFieldsSlug = 'collapsible-fields' as const +export const conditionalLogicSlug = 'conditional-logic' as const +export const dateFieldsSlug = 'date-fields' as const +export const groupFieldsSlug = 'group-fields' as const +export const indexedFieldsSlug = 'indexed-fields' as const +export const jsonFieldsSlug = 'json-fields' as const +export const lexicalFieldsSlug = 'lexical-fields' as const +export const lexicalMigrateFieldsSlug = 'lexical-migrate-fields' as const +export const numberFieldsSlug = 'number-fields' as const +export const pointFieldsSlug = 'point-fields' as const +export const radioFieldsSlug = 'radio-fields' as const +export const relationshipFieldsSlug = 'relationship-fields' as const +export const richTextFieldsSlug = 'rich-text-fields' as const +export const rowFieldsSlug = 'row-fields' as const +export const selectFieldsSlug = 'select-fields' as const +export const tabsFieldsSlug = 'tabs-fields' as const +export const textFieldsSlug = 'text-fields' as const +export const uploadsSlug = 'uploads' as const +export const uploads2Slug = 'uploads2' as const +export const uploads3Slug = 'uploads3' as const +export const collectionSlugs = [ + usersSlug, + arrayFieldsSlug, + blockFieldsSlug, + checkboxFieldsSlug, + codeFieldsSlug, + collapsibleFieldsSlug, + conditionalLogicSlug, + dateFieldsSlug, + groupFieldsSlug, + indexedFieldsSlug, + jsonFieldsSlug, + lexicalFieldsSlug, + lexicalMigrateFieldsSlug, + numberFieldsSlug, + pointFieldsSlug, + radioFieldsSlug, + relationshipFieldsSlug, + richTextFieldsSlug, + rowFieldsSlug, + selectFieldsSlug, + tabsFieldsSlug, + textFieldsSlug, + uploadsSlug, + uploads2Slug, + uploads3Slug, +] diff --git a/test/helpers/configHelpers.ts b/test/helpers/configHelpers.ts index b90c6e2a65..f25f0fa439 100644 --- a/test/helpers/configHelpers.ts +++ b/test/helpers/configHelpers.ts @@ -40,8 +40,6 @@ export async function initPayloadTest(options: Options): Promise { + return sql.raw(`DELETE FROM ${table.dbName}`) + }) + + await db.drizzle.transaction(async (trx) => { + await Promise.all( + queries.map(async (query) => { + if (query) { + await trx.execute(query) + } + }), + ) + }) + } +} diff --git a/test/helpers/seed.ts b/test/helpers/seed.ts new file mode 100644 index 0000000000..f3e3dda164 --- /dev/null +++ b/test/helpers/seed.ts @@ -0,0 +1,95 @@ +import fs from 'fs' +import path from 'path' + +import { type Payload } from '../../packages/payload/src' +import { isMongoose } from './isMongoose' +import { resetDB } from './reset' +import { createSnapshot, dbSnapshot, restoreFromSnapshot } from './snapshot' + +type SeedFunction = (_payload: Payload) => Promise + +export async function seedDB({ + shouldResetDB, + _payload, + snapshotKey, + seedFunction, + uploadsDir, + collectionSlugs, +}: { + _payload: Payload + collectionSlugs: string[] + seedFunction: SeedFunction + shouldResetDB: boolean + /** + * Key to uniquely identify the kind of snapshot. Each test suite should pass in a unique key + */ + snapshotKey: string + uploadsDir?: string +}) { + /** + * Reset database and delete uploads directory + */ + if (shouldResetDB) { + let clearUploadsDirPromise: any = Promise.resolve() + if (uploadsDir) { + clearUploadsDirPromise = fs.promises + .access(uploadsDir) + .then(() => fs.promises.readdir(uploadsDir)) + .then((files) => + Promise.all(files.map((file) => fs.promises.rm(path.join(uploadsDir, file)))), + ) + .catch((error) => { + if (error.code !== 'ENOENT') { + // If the error is not because the directory doesn't exist + console.error('Error clearing the uploads directory:', error) + throw error + } + // If the directory does not exist, resolve the promise (nothing to clear) + return + }) + } + + await Promise.all([resetDB(_payload, collectionSlugs), clearUploadsDirPromise]) + } + + /** + * Mongoose & Postgres: Restore snapshot of old data if available + * + * Note for postgres: For postgres, this needs to happen AFTER the tables were created. + * This does not work if I run payload.db.init or payload.db.connect anywhere. Thus, when resetting the database, we are not dropping the schema, but are instead only deleting the table values + */ + let restored = false + if (dbSnapshot[snapshotKey] && Object.keys(dbSnapshot[snapshotKey]).length) { + await restoreFromSnapshot(_payload, snapshotKey, collectionSlugs) + restored = true + } + + /** + * Mongoose: Re-create indexes + * Postgres: No need for any action here, since we only delete the table data and no schemas + */ + // Dropping the db breaks indexes (on mongoose - did not test extensively on postgres yet), so we recreate them here + if (shouldResetDB) { + if (isMongoose(_payload)) { + await Promise.all([ + ...collectionSlugs.map(async (collectionSlug) => { + await _payload.db.collections[collectionSlug].createIndexes() + }), + ]) + } + } + + /** + * If a snapshot was restored, we don't need to seed the database + */ + if (restored) { + return + } + + /** + * Seed the database with data and save it to a snapshot + **/ + await seedFunction(_payload) + + await createSnapshot(_payload, snapshotKey, collectionSlugs) +} diff --git a/test/helpers/snapshot.ts b/test/helpers/snapshot.ts new file mode 100644 index 0000000000..dc65a101f7 --- /dev/null +++ b/test/helpers/snapshot.ts @@ -0,0 +1,118 @@ +import type { PgTable } from 'drizzle-orm/pg-core' + +import { sql } from 'drizzle-orm' + +import type { PostgresAdapter } from '../../packages/db-postgres/src/types' +import type { Payload } from '../../packages/payload/src' + +import { isMongoose } from './isMongoose' + +export const dbSnapshot = {} + +async function createMongooseSnapshot(collectionsObj, snapshotKey: string) { + const snapshot = {} + + // Assuming `collectionsObj` is an object where keys are names and values are collection references + for (const collectionName of Object.keys(collectionsObj)) { + const collection = collectionsObj[collectionName] + const documents = await collection.find({}).toArray() // Get all documents + snapshot[collectionName] = documents + } + + dbSnapshot[snapshotKey] = snapshot // Save the snapshot in memory +} + +async function restoreFromMongooseSnapshot(collectionsObj, snapshotKey: string) { + if (!dbSnapshot[snapshotKey]) { + throw new Error('No snapshot found to restore from.') + } + + // Assuming `collectionsObj` is an object where keys are names and values are collection references + for (const [name, documents] of Object.entries(dbSnapshot[snapshotKey])) { + const collection = collectionsObj[name] + // You would typically clear the collection here, but as per your requirement, you do it manually + if ((documents as any[]).length > 0) { + await collection.insertMany(documents) + } + } +} + +async function createDrizzleSnapshot(db: PostgresAdapter, snapshotKey: string) { + const snapshot = {} + + const schema: Record = db.drizzle._.schema + if (!schema) { + return + } + + for (const tableName in schema) { + const table = db.drizzle.query[tableName]['fullSchema'][tableName] //db.drizzle._.schema[tableName] + const records = await db.drizzle.select().from(table).execute() + snapshot[tableName] = records + } + + dbSnapshot[snapshotKey] = snapshot +} + +async function restoreFromDrizzleSnapshot(db: PostgresAdapter, snapshotKey: string) { + if (!dbSnapshot[snapshotKey]) { + throw new Error('No snapshot found to restore from.') + } + + const disableFKConstraintChecksQuery = sql.raw('SET session_replication_role = replica;') + const enableFKConstraintChecksQuery = sql.raw('SET session_replication_role = DEFAULT;') + + await db.drizzle.transaction(async (trx) => { + // Temporarily disable foreign key constraint checks + await trx.execute(disableFKConstraintChecksQuery) + try { + for (const tableName in dbSnapshot[snapshotKey]) { + const table = db.drizzle.query[tableName]['fullSchema'][tableName] + await trx.delete(table).execute() // This deletes all records from the table. Probably not necessary, as I'm deleting the table before restoring anyways + + const records = dbSnapshot[snapshotKey][tableName] + if (records.length > 0) { + await trx.insert(table).values(records).execute() + } + } + } catch (e) { + console.error(e) + } finally { + // Re-enable foreign key constraint checks + await trx.execute(enableFKConstraintChecksQuery) + } + }) +} + +export async function createSnapshot( + _payload: Payload, + snapshotKey: string, + collectionSlugs: string[], +) { + if (isMongoose(_payload)) { + const mongooseCollections = _payload.db.collections[collectionSlugs[0]].db.collections + + await createMongooseSnapshot(mongooseCollections, snapshotKey) + } else { + const db: PostgresAdapter = _payload.db as unknown as PostgresAdapter + await createDrizzleSnapshot(db, snapshotKey) + } +} + +/** + * Make sure to delete the db before calling this function + * @param _payload + */ +export async function restoreFromSnapshot( + _payload: Payload, + snapshotKey: string, + collectionSlugs: string[], +) { + if (isMongoose(_payload)) { + const mongooseCollections = _payload.db.collections[collectionSlugs[0]].db.collections + await restoreFromMongooseSnapshot(mongooseCollections, snapshotKey) + } else { + const db: PostgresAdapter = _payload.db as unknown as PostgresAdapter + await restoreFromDrizzleSnapshot(db, snapshotKey) + } +} diff --git a/test/versions/collections/Autosave.ts b/test/versions/collections/Autosave.ts index 56b454e927..cb5f4f0063 100644 --- a/test/versions/collections/Autosave.ts +++ b/test/versions/collections/Autosave.ts @@ -1,9 +1,9 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' -import { autosaveSlug } from '../shared' +import { autosaveCollectionSlug } from '../slugs' const AutosavePosts: CollectionConfig = { - slug: autosaveSlug, + slug: autosaveCollectionSlug, labels: { singular: 'Autosave Post', plural: 'Autosave Posts', diff --git a/test/versions/collections/Drafts.ts b/test/versions/collections/Drafts.ts index 7662846d47..be3787824b 100644 --- a/test/versions/collections/Drafts.ts +++ b/test/versions/collections/Drafts.ts @@ -1,7 +1,7 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' import { CustomPublishButton } from '../elements/CustomSaveButton' -import { draftSlug } from '../shared' +import { draftCollectionSlug } from '../slugs' const DraftPosts: CollectionConfig = { access: { @@ -98,7 +98,7 @@ const DraftPosts: CollectionConfig = { type: 'blocks', }, ], - slug: draftSlug, + slug: draftCollectionSlug, versions: { drafts: true, maxPerDoc: 35, diff --git a/test/versions/collections/Posts.ts b/test/versions/collections/Posts.ts index 182d8cf2b2..6b5864e28b 100644 --- a/test/versions/collections/Posts.ts +++ b/test/versions/collections/Posts.ts @@ -1,19 +1,19 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' -import { draftSlug, postSlug, versionSlug } from '../shared' +import { draftCollectionSlug, postCollectionSlug, versionCollectionSlug } from '../slugs' const Posts: CollectionConfig = { - slug: postSlug, + slug: postCollectionSlug, fields: [ { name: 'relationToVersions', type: 'relationship', - relationTo: versionSlug, + relationTo: versionCollectionSlug, }, { name: 'relationToDrafts', type: 'relationship', - relationTo: draftSlug, + relationTo: draftCollectionSlug, }, ], } diff --git a/test/versions/collections/Versions.ts b/test/versions/collections/Versions.ts index 5ad35b559b..aba548883f 100644 --- a/test/versions/collections/Versions.ts +++ b/test/versions/collections/Versions.ts @@ -1,9 +1,9 @@ import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types' -import { versionSlug } from '../shared' +import { versionCollectionSlug } from '../slugs' const VersionPosts: CollectionConfig = { - slug: versionSlug, + slug: versionCollectionSlug, admin: { useAsTitle: 'title', defaultColumns: ['title', 'description', 'createdAt'], diff --git a/test/versions/config.ts b/test/versions/config.ts index 8c89f3d8a2..e6f1fc7fc2 100644 --- a/test/versions/config.ts +++ b/test/versions/config.ts @@ -1,3 +1,5 @@ +import path from 'path' + import { buildConfigWithDefaults } from '../buildConfigWithDefaults' import AutosavePosts from './collections/Autosave' import DraftPosts from './collections/Drafts' @@ -5,7 +7,7 @@ import Posts from './collections/Posts' import VersionPosts from './collections/Versions' import AutosaveGlobal from './globals/Autosave' import DraftGlobal from './globals/Draft' -import { seed } from './seed' +import { clearAndSeedEverything } from './seed' export default buildConfigWithDefaults({ collections: [Posts, AutosavePosts, DraftPosts, VersionPosts], @@ -15,5 +17,19 @@ export default buildConfigWithDefaults({ defaultLocale: 'en', locales: ['en', 'es'], }, - onInit: seed, + admin: { + webpack: (config) => ({ + ...config, + resolve: { + ...config.resolve, + alias: { + ...config?.resolve?.alias, + fs: path.resolve(__dirname, './mocks/emptyModule.js'), + }, + }, + }), + }, + onInit: async (payload) => { + await clearAndSeedEverything(payload) + }, }) diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index 817b4f2220..1412df69da 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -27,20 +27,22 @@ import type { Page } from '@playwright/test' import { expect, test } from '@playwright/test' +import payload from '../../packages/payload/src' import wait from '../../packages/payload/src/utilities/wait' -import { globalSlug } from '../admin/shared' +import { globalSlug } from '../admin/slugs' import { changeLocale, exactText, findTableCell, selectTableRow } from '../helpers' import { AdminUrlUtil } from '../helpers/adminUrlUtil' import { initPayloadE2E } from '../helpers/configHelpers' +import { clearAndSeedEverything } from './seed' +import { titleToDelete } from './shared' import { autoSaveGlobalSlug, - autosaveSlug, + autosaveCollectionSlug, + draftCollectionSlug, draftGlobalSlug, - draftSlug, - titleToDelete, -} from './shared' +} from './slugs' -const { beforeAll, describe } = test +const { beforeAll, beforeEach, describe } = test describe('versions', () => { let page: Page @@ -55,10 +57,14 @@ describe('versions', () => { page = await context.newPage() }) + beforeEach(async () => { + await clearAndSeedEverything(payload) + }) + describe('draft collections', () => { beforeAll(() => { - url = new AdminUrlUtil(serverURL, draftSlug) - autosaveURL = new AdminUrlUtil(serverURL, autosaveSlug) + url = new AdminUrlUtil(serverURL, draftCollectionSlug) + autosaveURL = new AdminUrlUtil(serverURL, autosaveCollectionSlug) }) // This test has to run before bulk updates that will rename the title diff --git a/test/versions/globals/Autosave.ts b/test/versions/globals/Autosave.ts index 460791c388..3c24a983c7 100644 --- a/test/versions/globals/Autosave.ts +++ b/test/versions/globals/Autosave.ts @@ -1,6 +1,6 @@ import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' -import { autoSaveGlobalSlug } from '../shared' +import { autoSaveGlobalSlug } from '../slugs' const AutosaveGlobal: GlobalConfig = { slug: autoSaveGlobalSlug, diff --git a/test/versions/globals/Draft.ts b/test/versions/globals/Draft.ts index 0fe64b135a..f83e84705f 100644 --- a/test/versions/globals/Draft.ts +++ b/test/versions/globals/Draft.ts @@ -1,6 +1,6 @@ import type { GlobalConfig } from '../../../packages/payload/src/globals/config/types' -import { draftGlobalSlug } from '../shared' +import { draftGlobalSlug } from '../slugs' const DraftGlobal: GlobalConfig = { slug: draftGlobalSlug, diff --git a/test/versions/int.spec.ts b/test/versions/int.spec.ts index cedc178111..1b956bd6b2 100644 --- a/test/versions/int.spec.ts +++ b/test/versions/int.spec.ts @@ -6,7 +6,8 @@ import { initPayloadTest } from '../helpers/configHelpers' import AutosavePosts from './collections/Autosave' import configPromise from './config' import AutosaveGlobal from './globals/Autosave' -import { autosaveSlug, draftSlug } from './shared' +import { clearAndSeedEverything } from './seed' +import { autosaveCollectionSlug, draftCollectionSlug } from './slugs' let collectionLocalPostID: string let collectionLocalVersionID @@ -54,13 +55,7 @@ describe('Versions', () => { }) beforeEach(async () => { - // First: delete potential existing versions from previous tests - if (collectionLocalPostID) { - await payload.delete({ - id: collectionLocalPostID, - collection, - }) - } + await clearAndSeedEverything(payload) // now: initialize const autosavePost = await payload.create({ @@ -94,7 +89,7 @@ describe('Versions', () => { describe('Create', () => { it('should allow creating a draft with missing required field data', async () => { const draft = await payload.create({ - collection: autosaveSlug, + collection: autosaveCollectionSlug, data: { description: undefined, title: 'i have a title', @@ -213,13 +208,13 @@ describe('Versions', () => { it('should query drafts with sort', async () => { const draftsAscending = await payload.find({ - collection: draftSlug, + collection: draftCollectionSlug, draft: true, sort: 'title', }) const draftsDescending = await payload.find({ - collection: draftSlug, + collection: draftCollectionSlug, draft: true, sort: '-title', }) @@ -233,14 +228,14 @@ describe('Versions', () => { it('should `findVersions` with sort', async () => { const draftsAscending = await payload.findVersions({ - collection: draftSlug, + collection: draftCollectionSlug, draft: true, sort: 'createdAt', limit: 100, }) const draftsDescending = await payload.findVersions({ - collection: draftSlug, + collection: draftCollectionSlug, draft: true, sort: '-createdAt', limit: 100, @@ -257,7 +252,7 @@ describe('Versions', () => { describe('Restore', () => { it('should return `findVersions` in correct order', async () => { const somePost = await payload.create({ - collection: draftSlug, + collection: draftCollectionSlug, data: { description: 'description 1', title: 'first post', @@ -266,14 +261,14 @@ describe('Versions', () => { const updatedPost = await payload.update({ id: somePost.id, - collection: draftSlug, + collection: draftCollectionSlug, data: { title: 'This should be the latest version', }, }) const versions = await payload.findVersions({ - collection: draftSlug, + collection: draftCollectionSlug, where: { parent: { equals: somePost.id }, }, @@ -286,7 +281,7 @@ describe('Versions', () => { const updated = 'updated' const versionedPost = await payload.create({ - collection: draftSlug, + collection: draftCollectionSlug, data: { description: 'version description', title: 'version title', @@ -297,7 +292,7 @@ describe('Versions', () => { // @ts-ignore let updatedPost = await payload.update({ id: versionedPost.id, - collection: draftSlug, + collection: draftCollectionSlug, data: { blocksField: [ { @@ -313,7 +308,7 @@ describe('Versions', () => { // @ts-ignore updatedPost = await payload.update({ id: versionedPost.id, - collection: draftSlug, + collection: draftCollectionSlug, data: { blocksField: [ { @@ -336,7 +331,7 @@ describe('Versions', () => { // Make sure it was updated correctly const draftFromUpdatedPost = await payload.findByID({ id: versionedPost.id, - collection: draftSlug, + collection: draftCollectionSlug, draft: true, }) expect(draftFromUpdatedPost.title).toBe(title2) @@ -344,7 +339,7 @@ describe('Versions', () => { expect(draftFromUpdatedPost.blocksField[0].localized).toStrictEqual(updated) const versions = await payload.findVersions({ - collection: draftSlug, + collection: draftCollectionSlug, where: { parent: { equals: versionedPost.id, @@ -356,7 +351,7 @@ describe('Versions', () => { // restore to previous version const restoredVersion = await payload.restoreVersion({ id: versionToRestore.id, - collection: draftSlug, + collection: draftCollectionSlug, }) expect({ ...restoredVersion }).toMatchObject({ @@ -366,7 +361,7 @@ describe('Versions', () => { const latestDraft = await payload.findByID({ id: versionedPost.id, - collection: draftSlug, + collection: draftCollectionSlug, draft: true, }) @@ -435,7 +430,7 @@ describe('Versions', () => { }) describe('Delete', () => { let postToDelete - beforeAll(async () => { + beforeEach(async () => { postToDelete = await payload.create({ collection, data: { @@ -633,7 +628,7 @@ describe('Versions', () => { const updatedTitle2 = 'new title 2' let firstDraft - beforeAll(async () => { + beforeEach(async () => { // This will be created in the `draft-posts` collection firstDraft = await payload.create({ collection: 'draft-posts', @@ -780,14 +775,6 @@ describe('Versions', () => { describe('Collections - GraphQL', () => { beforeEach(async () => { - // First: delete potential existing versions from previous tests - if (collectionGraphQLPostID) { - await payload.delete({ - id: collectionGraphQLPostID, - collection, - }) - } - const description = 'autosave description' const query = `mutation { @@ -1182,7 +1169,7 @@ describe('Versions', () => { }) describe('Globals - GraphQL', () => { - beforeAll(async () => { + beforeEach(async () => { // language=graphql const update = `mutation { updateAutosaveGlobal(draft: true, data: { diff --git a/test/versions/mocks/emptyModule.js b/test/versions/mocks/emptyModule.js new file mode 100644 index 0000000000..b1c6ea436a --- /dev/null +++ b/test/versions/mocks/emptyModule.js @@ -0,0 +1 @@ +export default {} diff --git a/test/versions/seed.ts b/test/versions/seed.ts new file mode 100644 index 0000000000..6d9b6f9586 --- /dev/null +++ b/test/versions/seed.ts @@ -0,0 +1,86 @@ +import { type Payload } from '../../packages/payload/src' +import { devUser } from '../credentials' +import { seedDB } from '../helpers/seed' +import { titleToDelete } from './shared' +import { collectionSlugs, draftCollectionSlug } from './slugs' + +export async function clearAndSeedEverything(_payload: Payload) { + return await seedDB({ + snapshotKey: 'versionsTest', + shouldResetDB: true, + collectionSlugs, + _payload, + seedFunction: async (_payload) => { + const blocksField = [ + { + blockType: 'block', + localized: 'text', + text: 'text', + }, + ] + + await Promise.all([ + _payload.create({ + collection: 'users', + data: { + email: devUser.email, + password: devUser.password, + }, + }), + _payload.create({ + collection: draftCollectionSlug, + data: { + blocksField, + description: 'Description', + radio: 'test', + title: 'Draft Title', + }, + draft: true, + }), + ]) + + const { id: manyDraftsID } = await _payload.create({ + collection: draftCollectionSlug, + data: { + blocksField, + description: 'Description', + radio: 'test', + title: 'Title With Many Versions', + }, + draft: true, + }) + + for (let i = 0; i < 10; i++) { + await _payload.update({ + id: manyDraftsID, + collection: draftCollectionSlug, + data: { + title: `Title With Many Versions ${i + 2}`, + }, + }) + } + + await _payload.create({ + collection: draftCollectionSlug, + data: { + _status: 'published', + blocksField, + description: 'Description', + radio: 'test', + title: 'Published Title', + }, + draft: false, + }) + + await _payload.create({ + collection: draftCollectionSlug, + data: { + blocksField, + description: 'Description', + title: titleToDelete, + }, + draft: true, + }) + }, + }) +} diff --git a/test/versions/seed/index.ts b/test/versions/seed/index.ts deleted file mode 100644 index 435f71f7b4..0000000000 --- a/test/versions/seed/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { Config } from '../../../packages/payload/src/config/types' - -import { devUser } from '../../credentials' -import { draftSlug, titleToDelete } from '../shared' - -export const seed: Config['onInit'] = async (payload) => { - await payload.create({ - collection: 'users', - data: { - email: devUser.email, - password: devUser.password, - }, - }) - - const blocksField = [ - { - blockType: 'block', - localized: 'text', - text: 'text', - }, - ] - - await payload.create({ - collection: draftSlug, - data: { - blocksField, - description: 'Description', - radio: 'test', - title: 'Draft Title', - }, - draft: true, - }) - - const { id: manyDraftsID } = await payload.create({ - collection: draftSlug, - data: { - blocksField, - description: 'Description', - radio: 'test', - title: 'Title With Many Versions', - }, - draft: true, - }) - - for (let i = 0; i < 10; i++) { - await payload.update({ - id: manyDraftsID, - collection: draftSlug, - data: { - title: `Title With Many Versions ${i + 2}`, - }, - }) - } - - await payload.create({ - collection: draftSlug, - data: { - _status: 'published', - blocksField, - description: 'Description', - radio: 'test', - title: 'Published Title', - }, - draft: false, - }) - - await payload.create({ - collection: draftSlug, - data: { - blocksField, - description: 'Description', - title: titleToDelete, - }, - draft: true, - }) -} diff --git a/test/versions/shared.ts b/test/versions/shared.ts index c7cc832ca1..9d43a69d0a 100644 --- a/test/versions/shared.ts +++ b/test/versions/shared.ts @@ -1,9 +1 @@ -export const postSlug = 'posts' -export const draftSlug = 'draft-posts' -export const autosaveSlug = 'autosave-posts' -export const versionSlug = 'version-posts' - -export const autoSaveGlobalSlug = 'autosave-global' -export const draftGlobalSlug = 'draft-global' - export const titleToDelete = 'Title To Delete' diff --git a/test/versions/slugs.ts b/test/versions/slugs.ts new file mode 100644 index 0000000000..6eb1b50b57 --- /dev/null +++ b/test/versions/slugs.ts @@ -0,0 +1,19 @@ +export const autosaveCollectionSlug = 'autosave-posts' as const + +export const draftCollectionSlug = 'draft-posts' as const + +export const postCollectionSlug = 'posts' as const + +export const versionCollectionSlug = 'version-posts' as const + +export const collectionSlugs = [ + autosaveCollectionSlug, + draftCollectionSlug, + postCollectionSlug, + versionCollectionSlug, +] + +export const autoSaveGlobalSlug = 'autosave-global' as const +export const draftGlobalSlug = 'draft-global' as const + +export const globalSlugs = [autoSaveGlobalSlug, draftGlobalSlug]