mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
refactor: plugin loading logic (#3448)
This commit is contained in:
44
.github/workflows/build.yml
vendored
44
.github/workflows/build.yml
vendored
@@ -110,6 +110,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
|
- name: Build Plugins
|
||||||
|
run: yarn run build:plugins
|
||||||
- name: Build Core
|
- name: Build Core
|
||||||
run: yarn nx build @affine/core
|
run: yarn nx build @affine/core
|
||||||
- name: Upload core artifact
|
- name: Upload core artifact
|
||||||
@@ -207,6 +209,48 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
yarn exec concurrently -k -s first -n "SB,TEST" -c "magenta,blue" "yarn exec serve ./storybook-static -l 6006" "yarn exec wait-on tcp:6006 && yarn test"
|
yarn exec concurrently -k -s first -n "SB,TEST" -c "magenta,blue" "yarn exec serve ./storybook-static -l 6006" "yarn exec wait-on tcp:6006 && yarn test"
|
||||||
|
|
||||||
|
e2e-plugin-test:
|
||||||
|
name: E2E Plugin Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: development
|
||||||
|
needs: build-core
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
with:
|
||||||
|
playwright-install: true
|
||||||
|
electron-install: false
|
||||||
|
- name: Download core artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: core
|
||||||
|
path: ./apps/core/dist
|
||||||
|
- name: Run playwright tests
|
||||||
|
run: yarn e2e --forbid-only
|
||||||
|
working-directory: tests/affine-plugin
|
||||||
|
env:
|
||||||
|
COVERAGE: true
|
||||||
|
- name: Collect code coverage report
|
||||||
|
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
|
||||||
|
|
||||||
|
- name: Upload e2e test coverage results
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
files: ./.coverage/lcov.info
|
||||||
|
flags: e2e-plugin-test
|
||||||
|
name: affine
|
||||||
|
fail_ci_if_error: false
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: test-results-e2e-plugin
|
||||||
|
path: ./test-results
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
e2e-test:
|
e2e-test:
|
||||||
name: E2E Test
|
name: E2E Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@affine-test/fixtures": "workspace:*",
|
"@affine-test/fixtures": "workspace:*",
|
||||||
"@affine/component": "workspace:*",
|
"@affine/component": "workspace:*",
|
||||||
"@affine/copilot": "workspace:*",
|
|
||||||
"@affine/debug": "workspace:*",
|
"@affine/debug": "workspace:*",
|
||||||
"@affine/env": "workspace:*",
|
"@affine/env": "workspace:*",
|
||||||
"@affine/graphql": "workspace:*",
|
"@affine/graphql": "workspace:*",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import 'ses';
|
import 'ses';
|
||||||
|
|
||||||
import * as AFFiNEComponent from '@affine/component';
|
import * as AFFiNEComponent from '@affine/component';
|
||||||
|
import { DebugLogger } from '@affine/debug';
|
||||||
import { FormatQuickBar } from '@blocksuite/blocks';
|
import { FormatQuickBar } from '@blocksuite/blocks';
|
||||||
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
|
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
|
||||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||||
@@ -40,6 +41,14 @@ if (!process.env.COVERAGE) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const builtinPluginUrl = new Set([
|
||||||
|
'/plugins/bookmark',
|
||||||
|
'/plugins/copilot',
|
||||||
|
'/plugins/hello-world',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const logger = new DebugLogger('register-plugins');
|
||||||
|
|
||||||
const PluginProvider = ({ children }: PropsWithChildren) =>
|
const PluginProvider = ({ children }: PropsWithChildren) =>
|
||||||
React.createElement(
|
React.createElement(
|
||||||
Provider,
|
Provider,
|
||||||
@@ -142,95 +151,127 @@ const createGlobalThis = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const group = new DisposableGroup();
|
const group = new DisposableGroup();
|
||||||
const pluginList = (await (
|
|
||||||
await fetch(new URL(`./plugins/plugin-list.json`, window.location.origin))
|
declare global {
|
||||||
).json()) as { name: string; assets: string[]; release: boolean }[];
|
// eslint-disable-next-line no-var
|
||||||
|
var __pluginPackageJson__: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.__pluginPackageJson__ = [];
|
||||||
|
|
||||||
|
interface PluginLoadedEvent extends CustomEvent<{ plugins: unknown[] }> {}
|
||||||
|
// add to window
|
||||||
|
declare global {
|
||||||
|
interface WindowEventMap {
|
||||||
|
'plugin-loaded': PluginLoadedEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pluginList.map(({ name: plugin, release, assets }) => {
|
[...builtinPluginUrl].map(url => {
|
||||||
if (!release && process.env.NODE_ENV !== 'development') {
|
return fetch(`${url}/package.json`)
|
||||||
return Promise.resolve();
|
.then(async res => {
|
||||||
}
|
const packageJson = await res.json();
|
||||||
const pluginCompartment = new Compartment(createGlobalThis(), {});
|
const {
|
||||||
const pluginGlobalThis = pluginCompartment.globalThis;
|
name: pluginName,
|
||||||
const baseURL = new URL(`./plugins/${plugin}/`, window.location.origin);
|
affinePlugin: {
|
||||||
const entryURL = new URL('index.js', baseURL);
|
release,
|
||||||
rootStore.set(registeredPluginAtom, prev => [...prev, plugin]);
|
entry: { core },
|
||||||
return fetch(entryURL).then(async res => {
|
assets,
|
||||||
if (assets.length > 0) {
|
},
|
||||||
await Promise.all(
|
} = packageJson;
|
||||||
assets.map(async asset => {
|
globalThis.__pluginPackageJson__.push(packageJson);
|
||||||
if (asset.endsWith('.css')) {
|
logger.debug(`registering plugin ${pluginName}`);
|
||||||
const res = await fetch(new URL(asset, baseURL));
|
logger.debug(`package.json: ${packageJson}`);
|
||||||
if (res.ok) {
|
if (!release) {
|
||||||
// todo: how to put css file into sandbox?
|
return Promise.resolve();
|
||||||
return res.text().then(text => {
|
}
|
||||||
const style = document.createElement('style');
|
const pluginCompartment = new Compartment(createGlobalThis(), {});
|
||||||
style.setAttribute('plugin-id', plugin);
|
const pluginGlobalThis = pluginCompartment.globalThis;
|
||||||
style.textContent = text;
|
const baseURL = url;
|
||||||
document.head.appendChild(style);
|
const entryURL = `${baseURL}/${core}`;
|
||||||
});
|
rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]);
|
||||||
}
|
await fetch(entryURL).then(async res => {
|
||||||
return null;
|
if (assets.length > 0) {
|
||||||
} else {
|
await Promise.all(
|
||||||
return Promise.resolve();
|
assets.map(async (asset: string) => {
|
||||||
}
|
if (asset.endsWith('.css')) {
|
||||||
})
|
const res = await fetch(`${baseURL}/${asset}`);
|
||||||
);
|
if (res.ok) {
|
||||||
}
|
// todo: how to put css file into sandbox?
|
||||||
const codeText = await res.text();
|
return res.text().then(text => {
|
||||||
pluginCompartment.evaluate(codeText, {
|
const style = document.createElement('style');
|
||||||
__evadeHtmlCommentTest__: true,
|
style.setAttribute('plugin-id', pluginName);
|
||||||
});
|
style.textContent = text;
|
||||||
pluginGlobalThis.__INTERNAL__ENTRY = {
|
document.head.appendChild(style);
|
||||||
register: (part, callback) => {
|
});
|
||||||
if (part === 'headerItem') {
|
}
|
||||||
rootStore.set(headerItemsAtom, items => ({
|
return null;
|
||||||
...items,
|
} else {
|
||||||
[plugin]: callback as CallbackMap['headerItem'],
|
return Promise.resolve();
|
||||||
}));
|
}
|
||||||
} else if (part === 'editor') {
|
})
|
||||||
rootStore.set(editorItemsAtom, items => ({
|
);
|
||||||
...items,
|
|
||||||
[plugin]: callback as CallbackMap['editor'],
|
|
||||||
}));
|
|
||||||
} else if (part === 'window') {
|
|
||||||
rootStore.set(windowItemsAtom, items => ({
|
|
||||||
...items,
|
|
||||||
[plugin]: callback as CallbackMap['window'],
|
|
||||||
}));
|
|
||||||
} else if (part === 'setting') {
|
|
||||||
console.log('setting');
|
|
||||||
rootStore.set(settingItemsAtom, items => ({
|
|
||||||
...items,
|
|
||||||
[plugin]: callback as CallbackMap['setting'],
|
|
||||||
}));
|
|
||||||
} else if (part === 'formatBar') {
|
|
||||||
console.log('1');
|
|
||||||
FormatQuickBar.customElements.push((page, getBlockRange) => {
|
|
||||||
console.log('2');
|
|
||||||
const div = document.createElement('div');
|
|
||||||
(callback as CallbackMap['formatBar'])(div, page, getBlockRange);
|
|
||||||
return div;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown part: ${part}`);
|
|
||||||
}
|
}
|
||||||
},
|
const codeText = await res.text();
|
||||||
utils: {
|
pluginCompartment.evaluate(codeText, {
|
||||||
PluginProvider,
|
__evadeHtmlCommentTest__: true,
|
||||||
},
|
});
|
||||||
} satisfies PluginContext;
|
pluginGlobalThis.__INTERNAL__ENTRY = {
|
||||||
const dispose = pluginCompartment.evaluate(
|
register: (part, callback) => {
|
||||||
'exports.entry(__INTERNAL__ENTRY)'
|
logger.info(`Registering ${pluginName} to ${part}`);
|
||||||
);
|
if (part === 'headerItem') {
|
||||||
if (typeof dispose !== 'function') {
|
rootStore.set(headerItemsAtom, items => ({
|
||||||
throw new Error('Plugin entry must return a function');
|
...items,
|
||||||
}
|
[pluginName]: callback as CallbackMap['headerItem'],
|
||||||
pluginGlobalThis.__INTERNAL__ENTRY = undefined;
|
}));
|
||||||
group.add(dispose);
|
} else if (part === 'editor') {
|
||||||
});
|
rootStore.set(editorItemsAtom, items => ({
|
||||||
|
...items,
|
||||||
|
[pluginName]: callback as CallbackMap['editor'],
|
||||||
|
}));
|
||||||
|
} else if (part === 'window') {
|
||||||
|
rootStore.set(windowItemsAtom, items => ({
|
||||||
|
...items,
|
||||||
|
[pluginName]: callback as CallbackMap['window'],
|
||||||
|
}));
|
||||||
|
} else if (part === 'setting') {
|
||||||
|
rootStore.set(settingItemsAtom, items => ({
|
||||||
|
...items,
|
||||||
|
[pluginName]: callback as CallbackMap['setting'],
|
||||||
|
}));
|
||||||
|
} else if (part === 'formatBar') {
|
||||||
|
FormatQuickBar.customElements.push((page, getBlockRange) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
(callback as CallbackMap['formatBar'])(
|
||||||
|
div,
|
||||||
|
page,
|
||||||
|
getBlockRange
|
||||||
|
);
|
||||||
|
return div;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown part: ${part}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
utils: {
|
||||||
|
PluginProvider,
|
||||||
|
},
|
||||||
|
} satisfies PluginContext;
|
||||||
|
const dispose = pluginCompartment.evaluate(
|
||||||
|
'exports.entry(__INTERNAL__ENTRY)'
|
||||||
|
);
|
||||||
|
if (typeof dispose !== 'function') {
|
||||||
|
throw new Error('Plugin entry must return a function');
|
||||||
|
}
|
||||||
|
pluginGlobalThis.__INTERNAL__ENTRY = undefined;
|
||||||
|
group.add(dispose);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error(`error when fetch plugin from ${url}`, e);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
);
|
).then(() => {
|
||||||
|
console.info('All plugins loaded');
|
||||||
console.log('register plugins finished');
|
});
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
"tests/fixtures",
|
"tests/fixtures",
|
||||||
"tests/kit",
|
"tests/kit",
|
||||||
"tests/affine-legacy/*",
|
"tests/affine-legacy/*",
|
||||||
"tests/affine-local"
|
"tests/affine-local",
|
||||||
|
"tests/affine-plugin"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.16.1 <19.0.0"
|
"node": ">=18.16.1 <19.0.0"
|
||||||
@@ -20,7 +21,6 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "dev-core",
|
"dev": "dev-core",
|
||||||
"dev:electron": "yarn workspace @affine/electron dev:app",
|
"dev:electron": "yarn workspace @affine/electron dev:app",
|
||||||
"dev:plugins": "./apps/electron/scripts/plugins/dev-plugins.mjs",
|
|
||||||
"build": "yarn nx build @affine/core",
|
"build": "yarn nx build @affine/core",
|
||||||
"build:electron": "yarn nx build @affine/electron",
|
"build:electron": "yarn nx build @affine/electron",
|
||||||
"build:storage": "yarn nx run-many -t build -p @affine/storage",
|
"build:storage": "yarn nx run-many -t build -p @affine/storage",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ok } from 'node:assert';
|
import { ok } from 'node:assert';
|
||||||
import { existsSync } from 'node:fs';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { parseArgs } from 'node:util';
|
import { parseArgs } from 'node:util';
|
||||||
|
|
||||||
@@ -109,8 +108,6 @@ const serverOutDir = path.resolve(
|
|||||||
plugin
|
plugin
|
||||||
);
|
);
|
||||||
|
|
||||||
const pluginListJsonPath = path.resolve(outDir, 'plugin-list.json');
|
|
||||||
|
|
||||||
const coreEntry = path.resolve(pluginDir, json.affinePlugin.entry.core);
|
const coreEntry = path.resolve(pluginDir, json.affinePlugin.entry.core);
|
||||||
if (json.affinePlugin.entry.server) {
|
if (json.affinePlugin.entry.server) {
|
||||||
const serverEntry = path.resolve(pluginDir, json.affinePlugin.entry.server);
|
const serverEntry = path.resolve(pluginDir, json.affinePlugin.entry.server);
|
||||||
@@ -162,41 +159,23 @@ await build({
|
|||||||
vanillaExtractPlugin(),
|
vanillaExtractPlugin(),
|
||||||
react(),
|
react(),
|
||||||
{
|
{
|
||||||
name: 'generate-list-json',
|
name: 'generate-package.json',
|
||||||
async generateBundle() {
|
async generateBundle() {
|
||||||
if (!existsSync(outDir)) {
|
const packageJson = {
|
||||||
await mkdir(outDir, { recursive: true });
|
name: json.name,
|
||||||
}
|
affinePlugin: {
|
||||||
const file = await open(pluginListJsonPath, 'as+', 0o777);
|
release: json.affinePlugin.release,
|
||||||
const txt = await file.readFile({
|
entry: {
|
||||||
encoding: 'utf-8',
|
core: 'index.js',
|
||||||
|
},
|
||||||
|
assets: [...metadata.assets],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.emitFile({
|
||||||
|
type: 'asset',
|
||||||
|
fileName: 'package.json',
|
||||||
|
source: JSON.stringify(packageJson, null, 2),
|
||||||
});
|
});
|
||||||
if (!txt) {
|
|
||||||
console.log('generate plugin-list.json');
|
|
||||||
await file.write(
|
|
||||||
JSON.stringify([
|
|
||||||
{
|
|
||||||
release: json.affinePlugin.release,
|
|
||||||
name: plugin,
|
|
||||||
assets: [...metadata.assets],
|
|
||||||
},
|
|
||||||
])
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log('modify plugin-list.json');
|
|
||||||
const list = JSON.parse(txt);
|
|
||||||
const index = list.findIndex((item: any) => item.name === plugin);
|
|
||||||
if (index === -1) {
|
|
||||||
list.push({
|
|
||||||
release: json.affinePlugin.release,
|
|
||||||
name: plugin,
|
|
||||||
assets: [...metadata.assets],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
list[index].assets = [...metadata.assets];
|
|
||||||
}
|
|
||||||
await file.write(JSON.stringify(list), 0);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@affine/copilot",
|
"name": "@affine/copilot-plugin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"affinePlugin": {
|
"affinePlugin": {
|
||||||
"release": false,
|
"release": false,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IconButton } from '@affine/component';
|
import { IconButton } from '@affine/component';
|
||||||
import { SendIcon } from '@blocksuite/icons';
|
import { SendIcon } from '@blocksuite/icons';
|
||||||
import { contentLayoutAtom } from '@toeverything/plugin-infra/atom';
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { Suspense, useCallback, useState } from 'react';
|
import { Suspense, useCallback, useState } from 'react';
|
||||||
@@ -62,11 +61,7 @@ const DetailContentImpl = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const DetailContent = (): ReactElement => {
|
export const DetailContent = (): ReactElement => {
|
||||||
const layout = useAtomValue(contentLayoutAtom);
|
|
||||||
const key = useAtomValue(openAIApiKeyAtom);
|
const key = useAtomValue(openAIApiKeyAtom);
|
||||||
if (layout === 'editor' || layout.second !== 'copilot') {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return <span>Please set OpenAI API Key in the debug panel.</span>;
|
return <span>Please set OpenAI API Key in the debug panel.</span>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ export const HeaderItem = (): ReactElement => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
onClick={useCallback(
|
onClick={useCallback(
|
||||||
() =>
|
() =>
|
||||||
|
// todo: abstract a context function to open a new tab
|
||||||
setLayout(layout => {
|
setLayout(layout => {
|
||||||
if (layout === 'editor') {
|
if (layout === 'editor') {
|
||||||
return {
|
return {
|
||||||
direction: 'horizontal',
|
direction: 'horizontal',
|
||||||
first: 'editor',
|
first: 'editor',
|
||||||
second: 'copilot',
|
second: '@affine/copilot',
|
||||||
splitPercentage: 70,
|
splitPercentage: 70,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
31
tests/affine-plugin/e2e/basic.spec.ts
Normal file
31
tests/affine-plugin/e2e/basic.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { test } from '@affine-test/kit/playwright';
|
||||||
|
import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||||
|
import { waitEditorLoad } from '@affine-test/kit/utils/page-logic';
|
||||||
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('plugin should exist', async ({ page }) => {
|
||||||
|
await openHomePage(page);
|
||||||
|
await waitEditorLoad(page);
|
||||||
|
await page.route('**/plugins/**/package.json', route => route.fetch(), {
|
||||||
|
times: 3,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(50);
|
||||||
|
const packageJson = await page.evaluate(() => {
|
||||||
|
// @ts-expect-error
|
||||||
|
return window.__pluginPackageJson__;
|
||||||
|
});
|
||||||
|
expect(packageJson).toEqual([
|
||||||
|
{
|
||||||
|
name: '@affine/bookmark-plugin',
|
||||||
|
affinePlugin: expect.anything(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '@affine/copilot-plugin',
|
||||||
|
affinePlugin: expect.anything(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '@affine/hello-world-plugin',
|
||||||
|
affinePlugin: expect.anything(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
13
tests/affine-plugin/package.json
Normal file
13
tests/affine-plugin/package.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "@affine-test/affine-plugin",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"e2e": "yarn playwright test"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@affine-test/fixtures": "workspace:*",
|
||||||
|
"@affine-test/kit": "workspace:*",
|
||||||
|
"@playwright/test": "^1.36.2"
|
||||||
|
},
|
||||||
|
"version": "0.7.0-canary.59"
|
||||||
|
}
|
||||||
63
tests/affine-plugin/playwright.config.ts
Normal file
63
tests/affine-plugin/playwright.config.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import type {
|
||||||
|
PlaywrightTestConfig,
|
||||||
|
PlaywrightWorkerOptions,
|
||||||
|
} from '@playwright/test';
|
||||||
|
// import { devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// require('dotenv').config();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
const config: PlaywrightTestConfig = {
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
timeout: process.env.CI ? 50_000 : 30_000,
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:8080/',
|
||||||
|
browserName:
|
||||||
|
(process.env.BROWSER as PlaywrightWorkerOptions['browserName']) ??
|
||||||
|
'chromium',
|
||||||
|
permissions: ['clipboard-read', 'clipboard-write'],
|
||||||
|
viewport: { width: 1440, height: 800 },
|
||||||
|
actionTimeout: 5 * 1000,
|
||||||
|
locale: 'en-US',
|
||||||
|
// Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer
|
||||||
|
// You can open traces locally(`npx playwright show-trace trace.zip`)
|
||||||
|
// or in your browser on [Playwright Trace Viewer](https://trace.playwright.dev/).
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
// Record video only when retrying a test for the first time.
|
||||||
|
video: 'on-first-retry',
|
||||||
|
},
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
workers: 4,
|
||||||
|
retries: 1,
|
||||||
|
// 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot'
|
||||||
|
// default 'list' when running locally
|
||||||
|
// See https://playwright.dev/docs/test-reporters#github-actions-annotations
|
||||||
|
reporter: process.env.CI ? 'github' : 'list',
|
||||||
|
|
||||||
|
webServer: [
|
||||||
|
// Intentionally not building the web, reminds you to run it by yourself.
|
||||||
|
{
|
||||||
|
command: 'yarn run start:web-static',
|
||||||
|
port: 8080,
|
||||||
|
timeout: 120 * 1000,
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
env: {
|
||||||
|
COVERAGE: process.env.COVERAGE || 'false',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.CI) {
|
||||||
|
config.retries = 3;
|
||||||
|
config.workers = '50%';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config;
|
||||||
16
tests/affine-plugin/tsconfig.json
Normal file
16
tests/affine-plugin/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"outDir": "lib"
|
||||||
|
},
|
||||||
|
"include": ["e2e"],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../kit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../fixtures"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -155,6 +155,9 @@
|
|||||||
{
|
{
|
||||||
"path": "./tests/affine-local"
|
"path": "./tests/affine-local"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "./tests/affine-plugin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "./tests/affine-legacy/0.7.0-canary.18"
|
"path": "./tests/affine-legacy/0.7.0-canary.18"
|
||||||
}
|
}
|
||||||
|
|||||||
15
yarn.lock
15
yarn.lock
@@ -48,6 +48,16 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@affine-test/affine-plugin@workspace:tests/affine-plugin":
|
||||||
|
version: 0.0.0-use.local
|
||||||
|
resolution: "@affine-test/affine-plugin@workspace:tests/affine-plugin"
|
||||||
|
dependencies:
|
||||||
|
"@affine-test/fixtures": "workspace:*"
|
||||||
|
"@affine-test/kit": "workspace:*"
|
||||||
|
"@playwright/test": ^1.36.2
|
||||||
|
languageName: unknown
|
||||||
|
linkType: soft
|
||||||
|
|
||||||
"@affine-test/fixtures@workspace:*, @affine-test/fixtures@workspace:tests/fixtures":
|
"@affine-test/fixtures@workspace:*, @affine-test/fixtures@workspace:tests/fixtures":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@affine-test/fixtures@workspace:tests/fixtures"
|
resolution: "@affine-test/fixtures@workspace:tests/fixtures"
|
||||||
@@ -155,9 +165,9 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@affine/copilot@workspace:*, @affine/copilot@workspace:plugins/copilot":
|
"@affine/copilot-plugin@workspace:plugins/copilot":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@affine/copilot@workspace:plugins/copilot"
|
resolution: "@affine/copilot-plugin@workspace:plugins/copilot"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@affine/component": "workspace:*"
|
"@affine/component": "workspace:*"
|
||||||
"@toeverything/plugin-infra": "workspace:*"
|
"@toeverything/plugin-infra": "workspace:*"
|
||||||
@@ -183,7 +193,6 @@ __metadata:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@affine-test/fixtures": "workspace:*"
|
"@affine-test/fixtures": "workspace:*"
|
||||||
"@affine/component": "workspace:*"
|
"@affine/component": "workspace:*"
|
||||||
"@affine/copilot": "workspace:*"
|
|
||||||
"@affine/debug": "workspace:*"
|
"@affine/debug": "workspace:*"
|
||||||
"@affine/env": "workspace:*"
|
"@affine/env": "workspace:*"
|
||||||
"@affine/graphql": "workspace:*"
|
"@affine/graphql": "workspace:*"
|
||||||
|
|||||||
Reference in New Issue
Block a user