diff --git a/.changeset/config.json b/.changeset/config.json index 1304e4b714..1892e1a308 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -5,7 +5,7 @@ "fixed": [], "linked": [], "access": "restricted", - "baseBranch": "feat/filesystem_and_search", + "baseBranch": "feat/cloud-sync", "updateInternalDependencies": "patch", "ignore": [] } diff --git a/.eslintignore b/.eslintignore index 63e7a67a1e..f2e3541e4c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,3 +4,5 @@ **/node_modules/** .github/** **/__tests__/** +**/tests/** + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 3fcc45a41b..3bb05d3646 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,9 +1,7 @@ --- name: I have a question about: Feel free to ask us your questions! -title: "[Question]" +title: '[Question]' labels: '' assignees: '' - --- - diff --git a/.github/workflows/changlog.yml b/.github/workflows/changlog.yml index 75adf3230f..6a4ced3e20 100644 --- a/.github/workflows/changlog.yml +++ b/.github/workflows/changlog.yml @@ -2,7 +2,7 @@ name: Pathfinder changelog on: push: - branches: [feat/filesystem_and_search, master] + branches: [feat/cloud-sync, master] # Cancels all previous workflow runs for pull requests that have not completed. # See https://docs.github.com/en/actions/using-jobs/using-concurrency diff --git a/.github/workflows/temp_test.yml b/.github/workflows/temp_test.yml new file mode 100644 index 0000000000..a1fa93bdf0 --- /dev/null +++ b/.github/workflows/temp_test.yml @@ -0,0 +1,128 @@ +name: Pathfinder Check + +on: + push: + branches: [feat/cloud-sync] + pull_request: + branches: [feat/cloud-sync] + +# Cancels all previous workflow runs for pull requests that have not completed. +# See https://docs.github.com/en/actions/using-jobs/using-concurrency +concurrency: + # The concurrency group contains the workflow name and the branch name for + # pull requests or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +jobs: + build: + name: Build on Pull Request + if: github.ref != 'refs/heads/master' + runs-on: self-hosted + environment: development + + steps: + - uses: actions/checkout@v2 + - uses: pnpm/action-setup@v2 + with: + version: 'latest' + + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + registry-url: https://npm.pkg.github.com + scope: '@toeverything' + cache: 'pnpm' + + - run: node scripts/module-resolve/ci.js + + - name: Restore cache + uses: actions/cache@v3 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_GITHUB_AUTH_TOKEN }} + + - name: Build + run: pnpm build + env: + NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} + NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }} + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }} + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} + NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }} + NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }} + + - name: Export + run: pnpm export + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + path: ./packages/app/.next + + lint: + name: Lint and E2E Test + runs-on: ubuntu-latest + environment: development + needs: build + + steps: + - uses: actions/checkout@v2 + - uses: pnpm/action-setup@v2 + with: + version: 'latest' + + - name: Use Node.js + uses: actions/setup-node@v2 + with: + node-version: 18.x + cache: 'pnpm' + + - name: Restore cache + uses: actions/cache@v3 + with: + path: | + .next/cache + # Generate a new cache whenever packages or source files change. + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + # If source files changed but packages didn't, rebuild from a prior cache. + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- + + - name: Install dependencies + run: pnpm install + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_GITHUB_AUTH_TOKEN }} + + - name: Download artifact + uses: actions/download-artifact@v3 + with: + name: artifact + path: packages/app/.next/ + + - name: Lint & E2E Test + run: | + pnpm lint --max-warnings=0 + PLAYWRIGHT_BROWSERS_PATH=0 npx playwright install chromium + PLAYWRIGHT_BROWSERS_PATH=0 pnpm test + PLAYWRIGHT_BROWSERS_PATH=0 pnpm test:dc + env: + NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} + NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} + NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }} + NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }} + NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} + NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }} + NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }} diff --git a/package.json b/package.json index 89999ff042..3db88747ff 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "test:e2e:codegen": "npx playwright codegen http://localhost:8080", "test:unit": "jest", "postinstall": "husky install", - "notify": "node --experimental-modules scripts/notify.mjs" + "notify": "node --experimental-modules scripts/notify.mjs", + "check:ci": "pnpm lint & pnpm test" }, "lint-staged": { "*": "prettier --write --ignore-unknown", diff --git a/packages/app/src/components/header/header.tsx b/packages/app/src/components/header/header.tsx index 21d99f01ce..919d89d7be 100644 --- a/packages/app/src/components/header/header.tsx +++ b/packages/app/src/components/header/header.tsx @@ -11,7 +11,7 @@ import { getWarningMessage, shouldShowWarning } from './utils'; import EditorOptionMenu from './header-right-items/editor-option-menu'; import TrashButtonGroup from './header-right-items/trash-button-group'; import ThemeModeSwitch from './header-right-items/theme-mode-switch'; -// import SyncUser from './header-right-items/sync-user'; +import SyncUser from './header-right-items/sync-user'; const BrowserWarning = ({ show, @@ -40,7 +40,7 @@ const HeaderRightItems: Record = { editorOptionMenu: , trashButtonGroup: , themeModeSwitch: , - syncUser: null, + syncUser: , }; export const Header = ({ diff --git a/packages/app/src/components/workspace-slider-bar/index.tsx b/packages/app/src/components/workspace-slider-bar/index.tsx index 0924752ac2..4954b5cd31 100644 --- a/packages/app/src/components/workspace-slider-bar/index.tsx +++ b/packages/app/src/components/workspace-slider-bar/index.tsx @@ -4,13 +4,14 @@ import { StyledArrowButton, StyledLink, StyledListItem, - // StyledListItemForWorkspace, + StyledListItemForWorkspace, StyledNewPageButton, StyledSliderBar, StyledSliderBarWrapper, StyledSubListItem, } from './style'; import { Arrow } from './icons'; +import { WorkspaceSelector } from './WorkspaceSelector'; import Collapse from '@mui/material/Collapse'; import { ArrowDownIcon, @@ -26,7 +27,6 @@ import { Tooltip } from '@/ui/tooltip'; import { useModal } from '@/providers/global-modal-provider'; import { useAppState } from '@/providers/app-state-provider/context'; import { IconButton } from '@/ui/button'; -// import { WorkspaceSelector } from './WorkspaceSelector'; import useLocalStorage from '@/hooks/use-local-storage'; import usePageMetaList from '@/hooks/use-page-meta-list'; import { usePageHelper } from '@/hooks/use-page-helper'; @@ -109,9 +109,9 @@ export const WorkSpaceSliderBar = () => { - {/* + - */} + { + return ( + +

Feature - {props.name}

+ {props.children} +
+ ); +}; + +export const Playground = () => { + return ( + <> + + + + + + +
    +
  • AFFiNE Demo
  • +
  • AFFiNE XXX
  • +
+ +
+ + +
Workspace Name /[Workspace Members Count]/[Workspace Avatar]
+
Cloud Sync [Yes/No]
+
Auth [Public/Private]
+
+ + + +
+ +
+ + +
+
+ Cloud Sync + +
+
+ + + +
    +
  • + terrychinaz@gmail +
  • +
+
+ + + + +
    +
    + + +
    Workspace Name
    + + +
    + + ); +}; + +Playground.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Playground; diff --git a/packages/data-center/src/apis/token.ts b/packages/data-center/src/apis/token.ts index b0acf68ee3..c509da0f33 100644 --- a/packages/data-center/src/apis/token.ts +++ b/packages/data-center/src/apis/token.ts @@ -60,7 +60,9 @@ class Token { } async initToken(token: string) { - this._setToken(await login({ token, type: 'Google' })); + const tokens = await login({ token, type: 'Google' }); + this._setToken(tokens); + return this._user; } async refreshToken(token?: string) { @@ -153,10 +155,27 @@ export const getAuthorizer = () => { const googleAuthProvider = new GoogleAuthProvider(); + const getToken = async () => { + const currentUser = firebaseAuth.currentUser; + if (currentUser) { + await currentUser.getIdTokenResult(true); + if (!currentUser.isAnonymous) { + return currentUser.getIdToken(); + } + } + return; + }; + const signInWithGoogle = async () => { - const user = await signInWithPopup(firebaseAuth, googleAuthProvider); - const idToken = await user.user.getIdToken(); - await token.initToken(idToken); + const idToken = await getToken(); + if (idToken) { + await token.initToken(idToken); + } else { + const user = await signInWithPopup(firebaseAuth, googleAuthProvider); + const idToken = await user.user.getIdToken(); + await token.initToken(idToken); + } + return firebaseAuth.currentUser; }; const onAuthStateChanged = (callback: (user: User | null) => void) => { diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index 47bf844f99..3e31e9f1d8 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -1,6 +1,6 @@ import assert from 'assert'; import { BlockSchema } from '@blocksuite/blocks/models'; -import { Workspace } from '@blocksuite/store'; +import { Workspace, Signal } from '@blocksuite/store'; import { getLogger } from './index.js'; import { getApis, Apis } from './apis/index.js'; @@ -16,6 +16,17 @@ type LoadConfig = { config?: Record; }; +export type DataCenterSignals = DataCenter['signals']; +type WorkspaceItem = { + // provider id + provider: string; + // data exists locally + locally: boolean; +}; +type WorkspaceLoadEvent = WorkspaceItem & { + workspace: string; +}; + export class DataCenter { private readonly _apis: Apis; private readonly _providers = new Map(); @@ -23,6 +34,11 @@ export class DataCenter { private readonly _config; private readonly _logger; + readonly signals = { + listAdd: new Signal(), + listRemove: new Signal(), + }; + static async init(debug: boolean): Promise { const dc = new DataCenter(debug); dc.addProvider(AffineProvider); @@ -36,6 +52,16 @@ export class DataCenter { this._config = getKVConfigure('sys'); this._logger = getLogger('dc'); this._logger.enabled = debug; + + this.signals.listAdd.on(e => { + this._config.set(`list:${e.workspace}`, { + provider: e.provider, + locally: e.locally, + }); + }); + this.signals.listRemove.on(workspace => { + this._config.delete(`list:${workspace}`); + }); } get apis(): Readonly { @@ -86,9 +112,9 @@ export class DataCenter { await provider.init({ apis: this._apis, config, - globalConfig: getKVConfigure(`provider:${providerId}`), debug: this._logger.enabled, logger: this._logger.extend(`${Provider.id}:${id}`), + signals: this.signals, workspace, }); await provider.initData(); @@ -97,6 +123,21 @@ export class DataCenter { return provider; } + async auth(providerId: string, globalConfig?: Record) { + const Provider = this._providers.get(providerId); + if (Provider) { + // initial configurator + const config = getKVConfigure(`provider:${providerId}`); + // set workspace configs + const values = Object.entries(globalConfig || {}); + if (values.length) await config.setMany(values); + + const logger = this._logger.extend(`auth:${providerId}`); + logger.enabled = this._logger.enabled; + await Provider.auth(config, logger, this.signals); + } + } + /** * load workspace data to memory * @param workspaceId workspace id @@ -150,24 +191,18 @@ export class DataCenter { } /** - * get workspace list + * get workspace list,return a map of workspace id and data state + * data state is also map, the key is the provider id, and the data exists locally when the value is true, otherwise it does not exist */ async list(): Promise>> { - const lists = await Promise.all( - Array.from(this._providers.entries()).map(([providerId, provider]) => - provider - .list(getKVConfigure(`provider:${providerId}`)) - .then(list => [providerId, list || []] as const) - ) - ); - - return lists.reduce((ret, [providerId, list]) => { - for (const [item, isLocal] of list) { - const workspace = ret[item] || {}; - workspace[providerId] = isLocal; - ret[item] = workspace; + const entries: [string, WorkspaceItem][] = await this._config.entries(); + return entries.reduce((acc, [k, i]) => { + if (k.startsWith('list:')) { + const key = k.slice(5); + acc[key] = acc[key] || {}; + acc[key][i.provider] = i.locally; } - return ret; + return acc; }, {} as Record>); } diff --git a/packages/data-center/src/index.ts b/packages/data-center/src/index.ts index 68aa74b916..0ca8f1cfee 100644 --- a/packages/data-center/src/index.ts +++ b/packages/data-center/src/index.ts @@ -7,6 +7,17 @@ const _initializeDataCenter = () => { return (debug = true) => { if (!_dataCenterInstance) { _dataCenterInstance = DataCenter.init(debug); + _dataCenterInstance.then(dc => { + try { + if (window) { + (window as any).dc = dc; + } + } catch (_) { + // ignore + } + + return dc; + }); } return _dataCenterInstance; diff --git a/packages/data-center/src/provider/affine/index.ts b/packages/data-center/src/provider/affine/index.ts index fef7a4d0a9..d2323f4ae1 100644 --- a/packages/data-center/src/provider/affine/index.ts +++ b/packages/data-center/src/provider/affine/index.ts @@ -1,11 +1,17 @@ import assert from 'assert'; -import { applyUpdate } from 'yjs'; +import { applyUpdate, Doc } from 'yjs'; -import type { InitialParams } from '../index.js'; -import { token, Callback } from '../../apis/index.js'; +import type { + ConfigStore, + DataCenterSignals, + InitialParams, + Logger, +} from '../index.js'; +import { token, Callback, getApis } from '../../apis/index.js'; import { LocalProvider } from '../local/index.js'; import { WebsocketProvider } from './sync.js'; +import { IndexedDBProvider } from '../local/indexeddb.js'; export class AffineProvider extends LocalProvider { static id = 'affine'; @@ -55,7 +61,14 @@ export class AffineProvider extends LocalProvider { } async initData() { - await super.initData(); + const databases = await indexedDB.databases(); + await super.initData( + // set locally to true if exists a same name db + databases + .map(db => db.name) + .filter(v => v) + .includes(this._workspace.room) + ); const workspace = this._workspace; const doc = workspace.doc; @@ -64,23 +77,29 @@ export class AffineProvider extends LocalProvider { if (workspace.room && token.isLogin) { try { - const updates = await this._apis.downloadWorkspace(workspace.room); - if (updates) { - await new Promise(resolve => { - doc.once('update', resolve); - applyUpdate(doc, new Uint8Array(updates)); - }); - // Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later - this._ws = new WebsocketProvider('/', workspace.room, doc); - await new Promise((resolve, reject) => { - // TODO: synced will also be triggered on reconnection after losing sync - // There needs to be an event mechanism to emit the synchronization state to the upper layer - assert(this._ws); - this._ws.once('synced', () => resolve()); - this._ws.once('lost-connection', () => resolve()); - this._ws.once('connection-error', () => reject()); - }); - } + // init data from cloud + await AffineProvider._initCloudDoc( + workspace.room, + doc, + this._logger, + this._signals + ); + + // Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later + this._ws = new WebsocketProvider('/', workspace.room, doc); + await new Promise((resolve, reject) => { + // TODO: synced will also be triggered on reconnection after losing sync + // There needs to be an event mechanism to emit the synchronization state to the upper layer + assert(this._ws); + this._ws.once('synced', () => resolve()); + this._ws.once('lost-connection', () => resolve()); + this._ws.once('connection-error', () => reject()); + }); + this._signals.listAdd.emit({ + workspace: workspace.room, + provider: this.id, + locally: true, + }); } catch (e) { this._logger('Failed to init cloud workspace', e); } @@ -91,4 +110,66 @@ export class AffineProvider extends LocalProvider { // just a workaround for yjs doc.getMap('space:meta'); } + + private static async _initCloudDoc( + workspace: string, + doc: Doc, + logger: Logger, + signals: DataCenterSignals + ) { + const apis = getApis(); + logger(`Loading ${workspace}...`); + const updates = await apis.downloadWorkspace(workspace); + if (updates) { + await new Promise(resolve => { + doc.once('update', resolve); + applyUpdate(doc, new Uint8Array(updates)); + }); + logger(`Loaded: ${workspace}`); + + // only add to list as online workspace + signals.listAdd.emit({ + workspace, + provider: this.id, + // at this time we always download full workspace + // but after we support sub doc, we can only download metadata + locally: false, + }); + } + } + + static async auth( + config: Readonly>, + logger: Logger, + signals: DataCenterSignals + ) { + const refreshToken = await config.get('token'); + if (refreshToken) { + await token.refreshToken(refreshToken); + if (token.isLogin && !token.isExpired) { + logger('check login success'); + // login success + return; + } + } + + logger('start login'); + // login with google + const apis = getApis(); + assert(apis.signInWithGoogle); + const user = await apis.signInWithGoogle(); + assert(user); + logger(`login success: ${user.displayName}`); + + // TODO: refresh local workspace data + const workspaces = await apis.getWorkspaces(); + await Promise.all( + workspaces.map(async ({ id }) => { + const doc = new Doc(); + const idb = new IndexedDBProvider(id, doc); + await idb.whenSynced; + await this._initCloudDoc(id, doc, logger, signals); + }) + ); + } } diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts index 3bba79fde1..847fcdf701 100644 --- a/packages/data-center/src/provider/base.ts +++ b/packages/data-center/src/provider/base.ts @@ -1,15 +1,21 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import type { Workspace } from '@blocksuite/store'; -import type { Apis, Logger, InitialParams, ConfigStore } from './index'; import type { BlobURL } from '@blocksuite/store/dist/blob/types'; +import type { + Apis, + DataCenterSignals, + Logger, + InitialParams, + ConfigStore, +} from './index'; export class BaseProvider { static id = 'base'; protected _apis!: Readonly; protected _config!: Readonly; - protected _globalConfig!: Readonly; protected _logger!: Logger; + protected _signals!: DataCenterSignals; protected _workspace!: Workspace; constructor() { @@ -23,8 +29,8 @@ export class BaseProvider { async init(params: InitialParams) { this._apis = params.apis; this._config = params.config; - this._globalConfig = params.globalConfig; this._logger = params.logger; + this._signals = params.signals; this._workspace = params.workspace; this._logger.enabled = params.debug; } @@ -58,6 +64,14 @@ export class BaseProvider { return this._workspace; } + static async auth( + _config: Readonly, + logger: Logger, + _signals: DataCenterSignals + ) { + logger("This provider doesn't require authentication"); + } + // get workspace list,return a map of workspace id and boolean // if value is true, it exists locally, otherwise it does not exist locally static async list( diff --git a/packages/data-center/src/provider/index.ts b/packages/data-center/src/provider/index.ts index f3fc53f9ad..5718b7eef0 100644 --- a/packages/data-center/src/provider/index.ts +++ b/packages/data-center/src/provider/index.ts @@ -1,6 +1,7 @@ import type { Workspace } from '@blocksuite/store'; import type { Apis } from '../apis'; +import type { DataCenterSignals } from '../datacenter'; import type { getLogger } from '../index'; import type { ConfigStore } from '../store'; @@ -9,13 +10,13 @@ export type Logger = ReturnType; export type InitialParams = { apis: Apis; config: Readonly; - globalConfig: Readonly; debug: boolean; logger: Logger; + signals: DataCenterSignals; workspace: Workspace; }; -export type { Apis, ConfigStore, Workspace }; +export type { Apis, ConfigStore, DataCenterSignals, Workspace }; export type { BaseProvider } from './base.js'; export { AffineProvider } from './affine/index.js'; export { LocalProvider } from './local/index.js'; diff --git a/packages/data-center/src/provider/local/index.ts b/packages/data-center/src/provider/local/index.ts index d2ced1b9ee..24bf9584a7 100644 --- a/packages/data-center/src/provider/local/index.ts +++ b/packages/data-center/src/provider/local/index.ts @@ -21,7 +21,7 @@ export class LocalProvider extends BaseProvider { this._blobs = blobs; } - async initData() { + async initData(locally = true) { assert(this._workspace.room); this._logger('Loading local data'); this._idb = new IndexedDBProvider( @@ -32,14 +32,19 @@ export class LocalProvider extends BaseProvider { await this._idb.whenSynced; this._logger('Local data loaded'); - await this._globalConfig.set(this._workspace.room, true); + this._signals.listAdd.emit({ + workspace: this._workspace.room, + provider: this.id, + locally, + }); } async clear() { + assert(this._workspace.room); await super.clear(); await this._blobs.clear(); await this._idb?.clearData(); - await this._globalConfig.delete(this._workspace.room!); + this._signals.listRemove.emit(this._workspace.room); } async destroy(): Promise { @@ -59,6 +64,10 @@ export class LocalProvider extends BaseProvider { config: Readonly> ): Promise | undefined> { const entries = await config.entries(); - return new Map(entries); + return new Map( + entries + .filter(([key]) => key.startsWith('list:')) + .map(([key, value]) => [key.slice(5), value]) + ); } } diff --git a/packages/data-center/tests/cloud/auth.spec.ts b/packages/data-center/tests/cloud/auth.spec.ts new file mode 100644 index 0000000000..f896dc99e6 --- /dev/null +++ b/packages/data-center/tests/cloud/auth.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from '@playwright/test'; + +import { getDataCenter } from '../utils.js'; + +import 'fake-indexeddb/auto'; + +test.describe('Auth', () => { + test('sign in', async () => {}); + + test('sign out', async () => {}); + + test('isLogin', async () => {}); + + test('getUserInfo', async () => {}); +}); diff --git a/packages/data-center/tests/cloud/collaborate.spec.ts b/packages/data-center/tests/cloud/collaborate.spec.ts new file mode 100644 index 0000000000..abe771109c --- /dev/null +++ b/packages/data-center/tests/cloud/collaborate.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from '@playwright/test'; + +import { getDataCenter } from '../utils.js'; + +import 'fake-indexeddb/auto'; + +test.describe('Collaborate', () => { + test('collaborate editor content', async () => {}); + + test('collaborate workspace name', async () => {}); + + test('collaborate workspace avatar', async () => {}); + + test('collaborate workspace list', async () => {}); +}); diff --git a/packages/data-center/tests/cloud/permission.spec.ts b/packages/data-center/tests/cloud/permission.spec.ts new file mode 100644 index 0000000000..69f40ad8f3 --- /dev/null +++ b/packages/data-center/tests/cloud/permission.spec.ts @@ -0,0 +1,17 @@ +import { test, expect } from '@playwright/test'; + +import { getDataCenter } from '../utils.js'; + +import 'fake-indexeddb/auto'; + +test.describe('Permission', () => { + test('get the public of workspace', async () => {}); + + test('make workspace public', async () => {}); + + test('make workspace private', async () => {}); + + test('un-login user open the public workspace ', async () => {}); + + test('un-login user open the private workspace ', async () => {}); +}); diff --git a/packages/data-center/tests/cloud/share.spec.ts b/packages/data-center/tests/cloud/share.spec.ts new file mode 100644 index 0000000000..1698e29eab --- /dev/null +++ b/packages/data-center/tests/cloud/share.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from '@playwright/test'; + +import { getDataCenter } from '../utils.js'; + +import 'fake-indexeddb/auto'; + +test.describe('Share', () => { + test('add(invite) member by email', async () => {}); + + test('accept invite member link', async () => {}); + + test('members list', async () => {}); + + test('delete member', async () => {}); +}); diff --git a/packages/data-center/tests/cloud/sync.spec.ts b/packages/data-center/tests/cloud/sync.spec.ts new file mode 100644 index 0000000000..f81cde5758 --- /dev/null +++ b/packages/data-center/tests/cloud/sync.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from '@playwright/test'; + +import { getDataCenter } from '../utils.js'; + +import 'fake-indexeddb/auto'; + +test.describe('Sync', () => { + test('get cloud the sync flag of workspace', async () => {}); + + test('enable [cloud sync feature]', async () => {}); + + test('close [cloud sync feature]', async () => {}); + + test('editor cloud storage', async () => {}); + + test('cloud sync is in-progress', async () => {}); + + test('cloud sync is completed', async () => {}); + + test('cloud sync is error', async () => {}); + + test('cloud storage is right', async () => {}); +}); diff --git a/packages/data-center/tests/datacenter.spec.ts b/packages/data-center/tests/datacenter.spec.ts deleted file mode 100644 index 661910583c..0000000000 --- a/packages/data-center/tests/datacenter.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { test, expect } from '@playwright/test'; - -import { getDataCenter } from './utils.js'; - -import 'fake-indexeddb/auto'; - -test('init data center', async () => { - const dataCenter = await getDataCenter(); - expect(dataCenter).toBeTruthy(); - await dataCenter.clear(); - - const workspace = await dataCenter.load('test1'); - expect(workspace).toBeTruthy(); -}); - -test('init data center singleton', async () => { - // data center is singleton - const [dc1, dc2] = await Promise.all([getDataCenter(), getDataCenter()]); - expect(dc1).toEqual(dc2); - - // load same workspace will get same instance - const [ws1, ws2] = await Promise.all([dc1.load('test1'), dc2.load('test1')]); - expect(ws1).toEqual(ws2); -}); - -test('should init error with unknown provider', async () => { - const dc = await getDataCenter(); - await dc.clear(); - - // load workspace with unknown provider will throw error - test.fail(); - await dc.load('test2', { providerId: 'not exist provider' }); -}); - -test.skip('init affine provider', async () => { - const dataCenter = await getDataCenter(); - await dataCenter.clear(); - - // load workspace with affine provider - // TODO: set constant token for testing - const workspace = await dataCenter.load('6', { - providerId: 'affine', - config: { token: 'YOUR_TOKEN' }, - }); - expect(workspace).toBeTruthy(); -}); - -test('list workspaces', async () => { - const dataCenter = await getDataCenter(); - await dataCenter.clear(); - - await Promise.all([ - dataCenter.load('test3'), - dataCenter.load('test4'), - dataCenter.load('test5'), - dataCenter.load('test6'), - ]); - - expect(await dataCenter.list()).toStrictEqual({ - test3: { local: true }, - test4: { local: true }, - test5: { local: true }, - test6: { local: true }, - }); - - await dataCenter.reload('test3', { providerId: 'affine' }); - expect(await dataCenter.list()).toStrictEqual({ - test3: { affine: true, local: true }, - test4: { local: true }, - test5: { local: true }, - test6: { local: true }, - }); -}); - -test('destroy workspaces', async () => { - const dataCenter = await getDataCenter(); - await dataCenter.clear(); - - // return new workspace if origin workspace is destroyed - const ws1 = await dataCenter.load('test7'); - await dataCenter.destroy('test7'); - const ws2 = await dataCenter.load('test7'); - expect(ws1 !== ws2).toBeTruthy(); - - // return new workspace if workspace is reload - const ws3 = await dataCenter.load('test8'); - const ws4 = await dataCenter.reload('test8', { providerId: 'affine' }); - expect(ws3 !== ws4).toBeTruthy(); -}); - -test('remove workspaces', async () => { - const dataCenter = await getDataCenter(); - await dataCenter.clear(); - - // remove workspace will remove workspace data - await Promise.all([dataCenter.load('test9'), dataCenter.load('test10')]); - await dataCenter.delete('test9'); - expect(await dataCenter.list()).toStrictEqual({ test10: { local: true } }); -}); diff --git a/packages/data-center/tests/local/attachment.spec.ts b/packages/data-center/tests/local/attachment.spec.ts new file mode 100644 index 0000000000..3a017080b6 --- /dev/null +++ b/packages/data-center/tests/local/attachment.spec.ts @@ -0,0 +1,13 @@ +import { test, expect } from '@playwright/test'; + +import { getDataCenter } from '../utils.js'; + +import 'fake-indexeddb/auto'; + +test.describe('Attachment', () => { + test('upload blob', async () => {}); + + test('get blob', async () => {}); + + test('remove blob', async () => {}); +}); diff --git a/packages/data-center/tests/local/import-export.spec.ts b/packages/data-center/tests/local/import-export.spec.ts new file mode 100644 index 0000000000..14d8a8a4f6 --- /dev/null +++ b/packages/data-center/tests/local/import-export.spec.ts @@ -0,0 +1,11 @@ +import { test, expect } from '@playwright/test'; + +import { getDataCenter } from '../utils.js'; + +import 'fake-indexeddb/auto'; + +test.describe('Import/Export Workspace', () => { + test('import workspace', async () => {}); + + test('export workspace', async () => {}); +}); diff --git a/packages/data-center/tests/local/init.spec.ts b/packages/data-center/tests/local/init.spec.ts new file mode 100644 index 0000000000..26d48ed40c --- /dev/null +++ b/packages/data-center/tests/local/init.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test'; + +import { getDataCenter } from '../utils.js'; + +import 'fake-indexeddb/auto'; + +test.describe('Init Data Center', () => { + test('init', async () => { + const dataCenter = await getDataCenter(); + expect(dataCenter).toBeTruthy(); + await dataCenter.clear(); + + const workspace = await dataCenter.load('test1'); + expect(workspace).toBeTruthy(); + }); + + test('init singleton', async () => { + // data center is singleton + const [dc1, dc2] = await Promise.all([getDataCenter(), getDataCenter()]); + expect(dc1).toEqual(dc2); + + // load same workspace will get same instance + const [ws1, ws2] = await Promise.all([ + dc1.load('test1'), + dc2.load('test1'), + ]); + expect(ws1).toEqual(ws2); + }); + + test('should init error with unknown provider', async () => { + const dc = await getDataCenter(); + await dc.clear(); + + // load workspace with unknown provider will throw error + test.fail(); + await dc.load('test2', { providerId: 'not exist provider' }); + }); + + test.skip('init affine provider', async () => { + const dataCenter = await getDataCenter(); + await dataCenter.clear(); + + // load workspace with affine provider + // TODO: set constant token for testing + const workspace = await dataCenter.load('6', { + providerId: 'affine', + config: { token: 'YOUR_TOKEN' }, + }); + expect(workspace).toBeTruthy(); + }); +}); diff --git a/packages/data-center/tests/local/search.spec.ts b/packages/data-center/tests/local/search.spec.ts new file mode 100644 index 0000000000..98ac1ca59a --- /dev/null +++ b/packages/data-center/tests/local/search.spec.ts @@ -0,0 +1,26 @@ +import assert from 'assert'; +import { test, expect } from '@playwright/test'; + +import { getDataCenter, waitOnce } from '../utils.js'; + +import 'fake-indexeddb/auto'; + +test.describe('Search', () => { + test('search result', async () => { + const dc = await getDataCenter(); + const workspace = await dc.load('test'); + + assert(workspace); + workspace.createPage('test'); + await waitOnce(workspace.signals.pageAdded); + const page = workspace.getPage('test'); + assert(page); + + const text = new page.Text(page, 'hello world'); + const blockId = page.addBlock({ flavour: 'affine:paragraph', text }); + + expect(workspace.search('hello')).toStrictEqual( + new Map([[blockId, 'test']]) + ); + }); +}); diff --git a/packages/data-center/tests/local/workspace.spec.ts b/packages/data-center/tests/local/workspace.spec.ts new file mode 100644 index 0000000000..771bf2ff69 --- /dev/null +++ b/packages/data-center/tests/local/workspace.spec.ts @@ -0,0 +1,70 @@ +import { test, expect } from '@playwright/test'; + +import { getDataCenter } from '../utils.js'; + +import 'fake-indexeddb/auto'; + +test.describe('Workspace', () => { + test('create', async () => {}); + + test('load', async () => {}); + + test('get workspace name', async () => {}); + test('set workspace name', async () => {}); + + test('get workspace avatar', async () => {}); + test('set workspace avatar', async () => {}); + + test('list', async () => { + const dataCenter = await getDataCenter(); + await dataCenter.clear(); + + await Promise.all([ + dataCenter.load('test3'), + dataCenter.load('test4'), + dataCenter.load('test5'), + dataCenter.load('test6'), + ]); + + expect(await dataCenter.list()).toStrictEqual({ + test3: { local: true }, + test4: { local: true }, + test5: { local: true }, + test6: { local: true }, + }); + + await dataCenter.reload('test3', { providerId: 'affine' }); + expect(await dataCenter.list()).toStrictEqual({ + test3: { affine: true }, + test4: { local: true }, + test5: { local: true }, + test6: { local: true }, + }); + }); + + test('destroy', async () => { + const dataCenter = await getDataCenter(); + await dataCenter.clear(); + + // return new workspace if origin workspace is destroyed + const ws1 = await dataCenter.load('test7'); + await dataCenter.destroy('test7'); + const ws2 = await dataCenter.load('test7'); + expect(ws1 !== ws2).toBeTruthy(); + + // return new workspace if workspace is reload + const ws3 = await dataCenter.load('test8'); + const ws4 = await dataCenter.reload('test8', { providerId: 'affine' }); + expect(ws3 !== ws4).toBeTruthy(); + }); + + test('remove', async () => { + const dataCenter = await getDataCenter(); + await dataCenter.clear(); + + // remove workspace will remove workspace data + await Promise.all([dataCenter.load('test9'), dataCenter.load('test10')]); + await dataCenter.delete('test9'); + expect(await dataCenter.list()).toStrictEqual({ test10: { local: true } }); + }); +}); diff --git a/packages/data-center/tests/utils.ts b/packages/data-center/tests/utils.ts index 878ccc57a8..1f4f128333 100644 --- a/packages/data-center/tests/utils.ts +++ b/packages/data-center/tests/utils.ts @@ -1,5 +1,9 @@ -export const getDataCenter = () => { - return import('../src/index.js').then(async dataCenter => - dataCenter.getDataCenter(false) - ); +import { Signal } from '@blocksuite/store'; + +export const getDataCenter = async () => { + const dataCenter = await import('../src/index.js'); + return await dataCenter.getDataCenter(false); }; + +export const waitOnce = (signal: Signal) => + new Promise(resolve => signal.once(val => resolve(val))); diff --git a/tests/contact-us.spec.ts b/tests/contact-us.spec.ts index 0de055787e..447aea1e02 100644 --- a/tests/contact-us.spec.ts +++ b/tests/contact-us.spec.ts @@ -4,11 +4,18 @@ import { loadPage } from './libs/load-page'; loadPage(); test.describe('Open contact us', () => { - test.skip('Click about us', async ({ page }) => { + test('Click about us', async ({ page }) => { const currentWorkspace = page.getByTestId('current-workspace'); await currentWorkspace.click(); // await page.waitForTimeout(1000); - await page.getByText('About AFFiNE').click(); + await page + .getByRole('tooltip', { + name: 'AFFiNE Log in to sync with affine About AFFiNE', + }) + .locator('div') + .filter({ hasText: 'About AFFiNE' }) + .nth(2) + .click(); const contactUsModal = page.locator( '[data-testid=contact-us-modal-content]' ); diff --git a/tests/libs/load-page.ts b/tests/libs/load-page.ts index 8428f27ec8..3546a42e0a 100644 --- a/tests/libs/load-page.ts +++ b/tests/libs/load-page.ts @@ -1,7 +1,11 @@ import { test } from '@playwright/test'; +import type { Page } from '@playwright/test'; +interface IType { + page: Page; +} export function loadPage() { - test.beforeEach(async ({ page }) => { + test.beforeEach(async ({ page }: IType) => { await page.goto('http://localhost:8080'); // waiting for page loading end await page.waitForSelector('#__next'); diff --git a/tests/libs/page-logic.ts b/tests/libs/page-logic.ts index 64b9f053c7..09385a4cb0 100644 --- a/tests/libs/page-logic.ts +++ b/tests/libs/page-logic.ts @@ -1,11 +1,13 @@ -export async function newPage(page) { +import type { Page } from '@playwright/test'; + +export async function newPage(page: Page) { return page.getByTestId('sliderBar').getByText('New Page').click(); } -export async function clickPageMoreActions(page) { +export async function clickPageMoreActions(page: Page) { return page .getByTestId('editor-header-items') .getByRole('button') - .nth(1) + .nth(2) .click(); } diff --git a/tests/local-first-workspace.spec.ts b/tests/local-first-workspace.spec.ts index 6aaf66786b..f3c1ba9e1c 100644 --- a/tests/local-first-workspace.spec.ts +++ b/tests/local-first-workspace.spec.ts @@ -3,7 +3,7 @@ import { loadPage } from './libs/load-page'; loadPage(); -test.describe.skip('Local first default workspace', () => { +test.describe('Local first default workspace', () => { test('Default workspace name', async ({ page }) => { const workspaceName = page.getByTestId('workspace-name'); expect(await workspaceName.textContent()).toBe('AFFiNE'); diff --git a/tests/login.spec.ts b/tests/login.spec.ts index 24e45092f5..39cda40b09 100644 --- a/tests/login.spec.ts +++ b/tests/login.spec.ts @@ -3,7 +3,7 @@ import { loadPage } from './libs/load-page'; loadPage(); -test.describe.skip('Login Flow', () => { +test.describe('Login Flow', () => { test('Open login modal by click current workspace', async ({ page }) => { await page.getByTestId('current-workspace').click(); await page.waitForTimeout(800); @@ -24,21 +24,22 @@ test.describe.skip('Login Flow', () => { .click(); }); - test('Open google firebase page', async ({ page }) => { - await page.getByTestId('current-workspace').click(); - await page.waitForTimeout(800); - // why don't we use waitForSelector, It seems that waitForSelector not stable? - await page.getByTestId('open-login-modal').click(); - await page.waitForTimeout(800); - const [firebasePage] = await Promise.all([ - page.waitForEvent('popup'), - page - .getByRole('button', { - name: 'Google Continue with Google Set up an AFFiNE account to sync data', - }) - .click(), - ]); + // not stable + // test.skip('Open google firebase page', async ({ page }) => { + // await page.getByTestId('current-workspace').click(); + // await page.waitForTimeout(800); + // // why don't we use waitForSelector, It seems that waitForSelector not stable? + // await page.getByTestId('open-login-modal').click(); + // await page.waitForTimeout(800); + // const [firebasePage] = await Promise.all([ + // page.waitForEvent('popup'), + // page + // .getByRole('button', { + // name: 'Google Continue with Google Set up an AFFiNE account to sync data', + // }) + // .click(), + // ]); - expect(firebasePage.url()).toContain('.firebaseapp.com/__/auth/handler'); - }); + // expect(firebasePage.url()).toContain('.firebaseapp.com/__/auth/handler'); + // }); });