Files
AppFlowy-Web/cypress/support/component.ts
Bartosz Sypytkowski 947740b683 feat: integrate new realtime sync protocol (#19)
* new sync protocolt

* refresh outline when folder change was detected

* add 250ms debounce for yjs doc update emits

* fix refresh outline on folder change

* remove existing SyncManager

* added update timestamp to lastUpdatedCollab info

* turn off http fetch in get doc page

* fix empty state vector binary parsing

* return existing sync context if possible

* update

* chore: load workspace database data before loadview

* chore: fixed the issue of database rows can not update

* add awareness to RegisterSyncContext type def

* chore: remove redundant logs

* chore: add display of collaborative users

* chore: display cursors

* chore: setup web socket reconnect options

* chore: init sync context only once

* revert reconnect

* chore: add blur

* chore: cache device id

* revert storage device id

* chore: refactor remote selection rendering and add cursor animation

* chore: refactor remote selection rendering

* chore: add blur

* chore: update cursor display logic, add device ID, optimize Yjs event handling

* chore: add cross-tab sync via broadcast channel

* chore: add text application logic, optimize selection transform handling, update ESLint rules

* chore: fix lint

* chore: others

* add heartbeat and ready state logging

* chore: add reconnect and listen connecting

* chore: fix conneting banner

* chore: modified the logic of adding recent

* echo-based heartbeat

* chore: remove condition of render cursors

* chore: modified generate cursor color

* chore: fix some test issues

* chore: use jest mock timers in sync tests

* chore: fix jest issue

* chore: fix the lint issue

---------

Co-authored-by: Kilu <lu@appflowy.io>
2025-08-07 11:43:56 +08:00

187 lines
5.4 KiB
TypeScript

// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import '@cypress/code-coverage/support';
import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command';
import 'cypress-real-events';
import 'cypress-real-events/support';
import { mount } from 'cypress/react18';
import '../../src/styles/global.css';
import './commands';
import './document';
// Alternatively you can use CommonJS syntax:
// require('./commands')
// Augment the Cypress namespace to include type definitions for
// your custom command.
// Alternatively, can be defined in cypress/support/component.d.ts
// with a <reference path="./component" /> at the top of your spec.
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
mount: typeof mount;
mockAPI: () => void;
mockDatabase: () => void;
mockCurrentWorkspace: () => void;
mockGetWorkspaceDatabases: () => void;
mockDocument: (id: string) => void;
clickOutside: () => void;
getTestingSelector: (testId: string) => Chainable<JQuery<HTMLElement>>;
selectText: (text: string) => void;
selectMultipleText: (texts: string[]) => void;
}
}
}
Cypress.Commands.add('mount', mount);
Cypress.Commands.add('getTestingSelector', (testId: string) => {
return cy.get(`[data-testid="${testId}"]`);
});
Cypress.Commands.add('clickOutside', () => {
cy.document().then((doc) => {
// [0, 0] is the top left corner of the window
const x = 0;
const y = 0;
const evt = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y,
});
// Dispatch the event
doc.elementFromPoint(x, y)?.dispatchEvent(evt);
});
});
function mergeRanges(ranges: Range[]): Range | null {
if (ranges.length === 0) return null;
const mergedRange = ranges[0].cloneRange();
for (let i = 1; i < ranges.length; i++) {
if (ranges[i].compareBoundaryPoints(Range.START_TO_START, mergedRange) < 0) {
mergedRange.setStart(ranges[i].startContainer, ranges[i].startOffset);
}
if (ranges[i].compareBoundaryPoints(Range.END_TO_END, mergedRange) > 0) {
mergedRange.setEnd(ranges[i].endContainer, ranges[i].endOffset);
}
}
return mergedRange;
}
Cypress.Commands.add('selectMultipleText', (texts: string[]) => {
const ranges: Range[] = [];
cy.window().then((win) => {
const promises = texts.map((text) => {
return new Cypress.Promise((resolve) => {
cy.contains(text).then(($el) => {
if (!$el) {
throw new Error(`The text "${text}" was not found in the document`);
}
const el = $el[0] as HTMLElement;
const document = el.ownerDocument;
const range = document.createRange();
const fullText = el.textContent || '';
const startIndex = fullText.indexOf(text);
const endIndex = startIndex + text.length;
if (startIndex !== -1 && endIndex !== -1) {
range.setStart(el.firstChild as Node, startIndex);
range.setEnd(el.firstChild as Node, endIndex);
ranges.push(range);
} else {
throw new Error(`The text "${text}" was not found in the element`);
}
resolve();
});
});
});
void Cypress.Promise.all(promises).then(() => {
const selection = win.getSelection();
if (selection) {
const mergedRange = mergeRanges(ranges);
selection.removeAllRanges();
if (mergedRange) {
selection.addRange(mergedRange);
}
}
cy.document().trigger('mouseup');
cy.document().trigger('selectionchange');
});
});
});
Cypress.Commands.add('selectText', (text: string) => {
cy.contains(text).then(($el) => {
if (!$el) {
throw new Error(`The text "${text}" was not found in the document`);
}
const el = $el[0] as HTMLElement;
const document = el.ownerDocument;
const range = document.createRange();
range.selectNodeContents(el);
const fullText = el.textContent || '';
const startIndex = fullText.indexOf(text);
const endIndex = startIndex + text.length;
if (startIndex !== -1 && endIndex !== -1) {
range.setStart(el.firstChild as HTMLElement, startIndex);
range.setEnd(el.firstChild as HTMLElement, endIndex);
const selection = document.getSelection() as Selection;
selection.removeAllRanges();
selection.addRange(range);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
$el.trigger('mouseup');
cy.document().trigger('selectionchange');
} else {
throw new Error(`The text "${text}" was not found in the element`);
}
});
});
// Example use:
// cy.mount(<MyComponent />)
addMatchImageSnapshotCommand({
failureThreshold: 0.03, // 允许 3% 的像素差异
failureThresholdType: 'percent',
customDiffConfig: { threshold: 0.1 },
capture: 'viewport',
});