test(editor): remove jsx snapshot (#9463)

This commit is contained in:
Saul-Mirone
2024-12-31 10:27:12 +00:00
parent 6c33eaace0
commit 36c1b103df
57 changed files with 2743 additions and 1656 deletions

View File

@@ -865,68 +865,6 @@ describe('getBlock', () => {
});
});
// Inline snapshot is not supported under describe.parallel config
describe('collection.exportJSX works', () => {
it('collection matches snapshot', () => {
const options = createTestOptions();
const collection = new DocCollection(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'doc:home' });
doc.addBlock('affine:page', { title: new doc.Text('hello') });
expect(collection.exportJSX()).toMatchInlineSnapshot(`
<affine:page
prop:count={0}
prop:items={[]}
prop:style={{}}
prop:title="hello"
/>
`);
});
it('empty collection matches snapshot', () => {
const options = createTestOptions();
const collection = new DocCollection(options);
collection.meta.initialize();
collection.createDoc({ id: 'doc:home' });
expect(collection.exportJSX()).toMatchInlineSnapshot('null');
});
it('collection with multiple blocks children matches snapshot', () => {
const options = createTestOptions();
const collection = new DocCollection(options);
collection.meta.initialize();
const doc = collection.createDoc({ id: 'doc:home' });
doc.load(() => {
const rootId = doc.addBlock('affine:page', {
title: new doc.Text(),
});
const noteId = doc.addBlock('affine:note', {}, rootId);
doc.addBlock('affine:paragraph', {}, noteId);
doc.addBlock('affine:paragraph', {}, noteId);
});
expect(collection.exportJSX()).toMatchInlineSnapshot(/* xml */ `
<affine:page
prop:count={0}
prop:items={[]}
prop:style={{}}
>
<affine:note>
<affine:paragraph
prop:type="text"
/>
<affine:paragraph
prop:type="text"
/>
</affine:note>
</affine:page>
`);
});
});
describe('flags', () => {
it('update flags', () => {
const options = createTestOptions();

View File

@@ -1,133 +0,0 @@
// checkout https://vitest.dev/guide/debugging.html for debugging tests
import { describe, expect, it } from 'vitest';
import { yDocToJSXNode } from '../utils/jsx.js';
describe('basic', () => {
it('serialized doc match snapshot', () => {
expect(
yDocToJSXNode(
{
'0': {
'sys:id': '0',
'sys:children': ['1'],
'sys:flavour': 'affine:page',
},
'1': {
'sys:id': '1',
'sys:children': [],
'sys:flavour': 'affine:paragraph',
'prop:text': [],
'prop:type': 'text',
},
},
'0'
)
).toMatchInlineSnapshot(`
<affine:page>
<affine:paragraph
prop:type="text"
/>
</affine:page>
`);
});
it('block with plain text should match snapshot', () => {
expect(
yDocToJSXNode(
{
'0': {
'sys:id': '0',
'sys:flavour': 'affine:page',
'sys:children': ['1'],
'prop:title': 'this is title',
},
'1': {
'sys:id': '2',
'sys:flavour': 'affine:paragraph',
'sys:children': [],
'prop:type': 'text',
'prop:text': [{ insert: 'just plain text' }],
},
},
'0'
)
).toMatchInlineSnapshot(`
<affine:page
prop:title="this is title"
>
<affine:paragraph
prop:text="just plain text"
prop:type="text"
/>
</affine:page>
`);
});
it('doc record match snapshot', () => {
expect(
yDocToJSXNode(
{
'0': {
'sys:id': '0',
'sys:flavour': 'affine:page',
'sys:children': ['1'],
'prop:title': 'this is title',
},
'1': {
'sys:id': '2',
'sys:flavour': 'affine:paragraph',
'sys:children': [],
'prop:type': 'text',
'prop:text': [
{ insert: 'this is ' },
{
insert: 'a ',
attributes: { link: 'http://www.example.com' },
},
{
insert: 'link',
attributes: { link: 'http://www.example.com', bold: true },
},
{ insert: ' with', attributes: { bold: true } },
{ insert: ' bold' },
],
},
},
'0'
)
).toMatchInlineSnapshot(`
<affine:page
prop:title="this is title"
>
<affine:paragraph
prop:text={
<>
<text
insert="this is "
/>
<text
insert="a "
link="http://www.example.com"
/>
<text
bold={true}
insert="link"
link="http://www.example.com"
/>
<text
bold={true}
insert=" with"
/>
<text
insert=" bold"
/>
</>
}
prop:type="text"
/>
</affine:page>
`);
});
});

View File

@@ -1,2 +0,0 @@
export { test } from './test.js';
export { DocCollectionAddonType } from './type.js';

View File

@@ -1,19 +0,0 @@
import type { DocCollection, DocCollectionOptions } from '../collection.js';
type DocCollectionConstructor<Keys extends string> = {
new (storeOptions: DocCollectionOptions): Omit<DocCollection, Keys>;
};
export type AddOn<Keys extends string> = (
originalClass: DocCollectionConstructor<Keys>,
context: ClassDecoratorContext
) => { new (storeOptions: DocCollectionOptions): unknown };
export type AddOnReturn<Keys extends string> = (
originalClass: DocCollectionConstructor<Keys>,
context: ClassDecoratorContext
) => typeof DocCollection;
export function addOnFactory<Keys extends string>(fn: AddOn<Keys>) {
return fn as AddOnReturn<Keys>;
}

View File

@@ -1,38 +0,0 @@
import { assertExists } from '@blocksuite/global/utils';
import type { JSXElement } from '../../utils/jsx.js';
import { serializeYDoc, yDocToJSXNode } from '../../utils/jsx.js';
import { addOnFactory } from './shared.js';
export interface TestAddon {
importDocSnapshot: (json: unknown, docId: string) => Promise<void>;
exportJSX: (blockId?: string, docId?: string) => JSXElement;
}
export const test = addOnFactory<keyof TestAddon>(
originalClass =>
class extends originalClass {
/** @internal Only for testing */
exportJSX(blockId?: string, docId = this.meta.docMetas.at(0)?.id) {
assertExists(docId);
const doc = this.doc.spaces.get(docId);
assertExists(doc);
const docJson = serializeYDoc(doc);
if (!docJson) {
throw new Error(`Doc ${docId} doesn't exist`);
}
const blockJson = docJson.blocks as Record<string, unknown>;
if (!blockId) {
const rootId = Object.keys(blockJson).at(0);
if (!rootId) {
return null;
}
blockId = rootId;
}
if (!blockJson[blockId]) {
return null;
}
return yDocToJSXNode(blockJson, blockId);
}
}
);

View File

@@ -1,7 +0,0 @@
import type { TestAddon } from './test.js';
export class DocCollectionAddonType implements TestAddon {
exportJSX!: TestAddon['exportJSX'];
importDocSnapshot!: TestAddon['importDocSnapshot'];
}

View File

@@ -23,7 +23,6 @@ import {
BlockSuiteDoc,
type RawAwarenessState,
} from '../yjs/index.js';
import { DocCollectionAddonType, test } from './addon/index.js';
import { BlockCollection, type GetDocOptions } from './doc/block-collection.js';
import type { Doc, Query } from './doc/index.js';
import type { IdGeneratorType } from './id.js';
@@ -73,10 +72,7 @@ export interface StackItem {
meta: Map<'cursor-location' | 'selection-state', unknown>;
}
// oxlint-disable-next-line
// @ts-ignore FIXME: typecheck error
@test
export class DocCollection extends DocCollectionAddonType {
export class DocCollection {
static Y = Y;
protected readonly _schema: Schema;
@@ -142,7 +138,6 @@ export class DocCollection extends DocCollectionAddonType {
},
logger = new NoopLogger(),
}: DocCollectionOptions) {
super();
this._schema = schema;
this.id = id || '';

View File

@@ -1,68 +0,0 @@
import { BlockModel } from '../schema/base.js';
function isBlockModel(a: unknown): a is BlockModel {
return a instanceof BlockModel;
}
/**
* Ported from https://github.com/vuejs/core/blob/main/packages/runtime-core/src/customFormatter.ts
*
* See [Custom Object Formatters in Chrome DevTools](https://docs.google.com/document/d/1FTascZXT9cxfetuPRT2eXPQKXui4nWFivUnS_335T3U)
*/
function initCustomFormatter() {
if (
!(process.env.NODE_ENV === 'development') ||
typeof window === 'undefined'
) {
return;
}
const bannerStyle = {
style:
'color: #eee; background: #3F6FDB; margin-right: 5px; padding: 2px; border-radius: 4px',
};
const typeStyle = {
style:
'color: #eee; background: #DB6D56; margin-right: 5px; padding: 2px; border-radius: 4px',
};
// custom formatter for Chrome
// https://www.mattzeunert.com/2016/02/19/custom-chrome-devtools-object-formatters.html
const formatter = {
header(obj: unknown, config = { expand: false }) {
if (!isBlockModel(obj) || config.expand) {
return null;
}
if (obj.text) {
return [
'div',
{},
['span', bannerStyle, obj.constructor.name],
['span', typeStyle, obj.flavour],
obj.text.toString(),
];
}
return [
'div',
{},
['span', bannerStyle, obj.constructor.name],
['span', typeStyle, obj.flavour],
];
},
hasBody() {
return true;
},
body(obj: unknown) {
return ['object', { object: obj, config: { expand: true } }];
},
};
if ((window as any).devtoolsFormatters) {
(window as any).devtoolsFormatters.push(formatter);
} else {
(window as any).devtoolsFormatters = [formatter];
}
}
initCustomFormatter();

View File

@@ -1,165 +0,0 @@
import * as Y from 'yjs';
type DocRecord = Record<
string,
{
'sys:id': string;
'sys:flavour': string;
'sys:children': string[];
[id: string]: unknown;
}
>;
export interface JSXElement {
// Ad-hoc for `ReactTestComponent` identify.
// Use ReactTestComponent serializer prevent snapshot be be wrapped in a string, which cases " to be escaped.
// See https://github.com/facebook/jest/blob/f1263368cc85c3f8b70eaba534ddf593392c44f3/packages/pretty-format/src/plugins/ReactTestComponent.ts#L78-L79
$$typeof: symbol | 0xea71357;
type: string;
props: { 'prop:text'?: string | JSXElement } & Record<string, unknown>;
children?: null | (JSXElement | string | number)[];
}
// Ad-hoc for `ReactTestComponent` identify.
// See https://github.com/facebook/jest/blob/f1263368cc85c3f8b70eaba534ddf593392c44f3/packages/pretty-format/src/plugins/ReactTestComponent.ts#L26-L29
const testSymbol = Symbol.for('react.test.json');
function isValidRecord(data: unknown): data is DocRecord {
if (typeof data !== 'object' || data === null) {
return false;
}
// TODO enhance this check
return true;
}
const IGNORED_PROPS = new Set([
'sys:id',
'sys:version',
'sys:flavour',
'sys:children',
'prop:xywh',
'prop:cells',
'prop:elements',
]);
export function yDocToJSXNode(
serializedDoc: Record<string, unknown>,
nodeId: string
): JSXElement {
if (!isValidRecord(serializedDoc)) {
throw new Error('Failed to parse doc record! Invalid data.');
}
const node = serializedDoc[nodeId];
if (!node) {
throw new Error(
`Failed to parse doc record! Node not found! id: ${nodeId}.`
);
}
// TODO maybe need set PascalCase
const flavour = node['sys:flavour'];
// TODO maybe need check children recursively nested
const children = node['sys:children'];
const props = Object.fromEntries(
Object.entries(node).filter(([key]) => !IGNORED_PROPS.has(key))
);
if ('prop:text' in props && props['prop:text'] instanceof Array) {
props['prop:text'] = parseDelta(props['prop:text'] as DeltaText);
}
if ('prop:title' in props && props['prop:title'] instanceof Array) {
props['prop:title'] = parseDelta(props['prop:title'] as DeltaText);
}
if ('prop:columns' in props && props['prop:columns'] instanceof Array) {
props['prop:columns'] = `Array [${props['prop:columns'].length}]`;
}
if ('prop:views' in props && props['prop:views'] instanceof Array) {
props['prop:views'] = `Array [${props['prop:views'].length}]`;
}
return {
$$typeof: testSymbol,
type: flavour,
props,
children: children?.map(id => yDocToJSXNode(serializedDoc, id)) ?? [],
};
}
export function serializeYDoc(doc: Y.Doc) {
const json: Record<string, unknown> = {};
doc.share.forEach((value, key) => {
if (value instanceof Y.Map) {
json[key] = serializeYMap(value);
} else {
json[key] = value.toJSON();
}
});
return json;
}
function serializeY(value: unknown): unknown {
if (value instanceof Y.Doc) {
return serializeYDoc(value);
}
if (value instanceof Y.Map) {
return serializeYMap(value);
}
if (value instanceof Y.Text) {
return serializeYText(value);
}
if (value instanceof Y.Array) {
return value.toArray().map(x => serializeY(x));
}
if (value instanceof Y.AbstractType) {
return value.toJSON();
}
return value;
}
function serializeYMap(map: Y.Map<unknown>) {
const json: Record<string, unknown> = {};
map.forEach((value, key) => {
json[key] = serializeY(value);
});
return json;
}
type DeltaText = {
insert: string;
attributes?: Record<string, unknown>;
}[];
function serializeYText(text: Y.Text): DeltaText {
const delta = text.toDelta();
return delta;
}
function parseDelta(text: DeltaText) {
if (!text.length) {
return undefined;
}
if (text.length === 1 && !text[0].attributes) {
// just plain text
return text[0].insert;
}
return {
// The `Symbol.for('react.fragment')` will render as `<React.Fragment>`
// so we use a empty string to render it as `<>`.
// But it will empty children ad `< />`
// so we return `undefined` directly if not delta text.
$$typeof: testSymbol, // Symbol.for('react.element'),
type: '', // Symbol.for('react.fragment'),
props: {},
children: text?.map(({ insert, attributes }) => ({
$$typeof: testSymbol,
type: 'text',
props: {
// Not place at `children` to avoid the trailing whitespace be trim by formatter.
insert,
...attributes,
},
})),
};
}