mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
test(editor): remove jsx snapshot (#9463)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -1,2 +0,0 @@
|
||||
export { test } from './test.js';
|
||||
export { DocCollectionAddonType } from './type.js';
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { TestAddon } from './test.js';
|
||||
|
||||
export class DocCollectionAddonType implements TestAddon {
|
||||
exportJSX!: TestAddon['exportJSX'];
|
||||
|
||||
importDocSnapshot!: TestAddon['importDocSnapshot'];
|
||||
}
|
||||
@@ -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 || '';
|
||||
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user