mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-27 18:53:43 +03:00
Merge pull request #680 from toeverything/feat/cloud-sync
feat: auth & sync
This commit is contained in:
commit
12804b0dea
@ -5,7 +5,7 @@
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "feat/filesystem_and_search",
|
||||
"baseBranch": "feat/cloud-sync",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
|
@ -4,3 +4,5 @@
|
||||
**/node_modules/**
|
||||
.github/**
|
||||
**/__tests__/**
|
||||
**/tests/**
|
||||
|
||||
|
4
.github/ISSUE_TEMPLATE/question.md
vendored
4
.github/ISSUE_TEMPLATE/question.md
vendored
@ -1,9 +1,7 @@
|
||||
---
|
||||
name: I have a question
|
||||
about: Feel free to ask us your questions!
|
||||
title: "[Question]"
|
||||
title: '[Question]'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
2
.github/workflows/changlog.yml
vendored
2
.github/workflows/changlog.yml
vendored
@ -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
|
||||
|
128
.github/workflows/temp_test.yml
vendored
Normal file
128
.github/workflows/temp_test.yml
vendored
Normal file
@ -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 }}
|
@ -16,7 +16,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",
|
||||
|
@ -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<HeaderRightItemNames, ReactNode> = {
|
||||
editorOptionMenu: <EditorOptionMenu key="editorOptionMenu" />,
|
||||
trashButtonGroup: <TrashButtonGroup key="trashButtonGroup" />,
|
||||
themeModeSwitch: <ThemeModeSwitch key="themeModeSwitch" />,
|
||||
syncUser: null,
|
||||
syncUser: <SyncUser key="syncUser" />,
|
||||
};
|
||||
|
||||
export const Header = ({
|
||||
|
@ -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 = () => {
|
||||
</Tooltip>
|
||||
|
||||
<StyledSliderBarWrapper data-testid="sliderBar">
|
||||
{/* <StyledListItemForWorkspace>
|
||||
<StyledListItemForWorkspace>
|
||||
<WorkspaceSelector />
|
||||
</StyledListItemForWorkspace> */}
|
||||
</StyledListItemForWorkspace>
|
||||
<StyledListItem
|
||||
data-testid="sliderBar-quickSearchButton"
|
||||
style={{ cursor: 'pointer' }}
|
||||
|
@ -0,0 +1,90 @@
|
||||
import { styled } from '@/styles';
|
||||
|
||||
import { ReactElement, ReactNode } from 'react';
|
||||
import WorkspaceLayout from '@/components/workspace-layout';
|
||||
import { Button } from '@/ui/button';
|
||||
|
||||
export const FeatureCardDiv = styled('section')({
|
||||
width: '800px',
|
||||
border: '1px #eee solid',
|
||||
margin: '20px auto',
|
||||
minHeight: '100px',
|
||||
padding: '15px',
|
||||
});
|
||||
const FeatureCard = (props: {
|
||||
name: string;
|
||||
children: ReactNode | ReactNode[];
|
||||
}) => {
|
||||
return (
|
||||
<FeatureCardDiv>
|
||||
<h1>Feature - {props.name}</h1>
|
||||
{props.children}
|
||||
</FeatureCardDiv>
|
||||
);
|
||||
};
|
||||
|
||||
export const Playground = () => {
|
||||
return (
|
||||
<>
|
||||
<FeatureCard name="Account">
|
||||
<Button>Sign In</Button>
|
||||
<Button>Sign Out</Button>
|
||||
</FeatureCard>
|
||||
|
||||
<FeatureCard name="Workspace List">
|
||||
<ul>
|
||||
<li>AFFiNE Demo</li>
|
||||
<li>AFFiNE XXX</li>
|
||||
</ul>
|
||||
<Button>New Workspace</Button>
|
||||
</FeatureCard>
|
||||
|
||||
<FeatureCard name="Active Workspace">
|
||||
<div>Workspace Name /[Workspace Members Count]/[Workspace Avatar]</div>
|
||||
<div>Cloud Sync [Yes/No]</div>
|
||||
<div>Auth [Public/Private]</div>
|
||||
<div>
|
||||
<Button>Update Workspace Name</Button>
|
||||
<Button>Upload Workspace Avatar</Button>
|
||||
<Button>Update Workspace Avatar</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button>Leave Workspace</Button>
|
||||
<Button>Delete Workspace </Button>
|
||||
</div>
|
||||
<div>
|
||||
Cloud Sync <Button>Enalbe</Button>
|
||||
<Button>Disable</Button>
|
||||
</div>
|
||||
</FeatureCard>
|
||||
|
||||
<FeatureCard name="Workspace Members">
|
||||
<Button>Add Member</Button>
|
||||
<ul>
|
||||
<li>
|
||||
terrychinaz@gmail <button>Delete Members</button>
|
||||
</li>
|
||||
</ul>
|
||||
</FeatureCard>
|
||||
|
||||
<FeatureCard name="Cloud Search">
|
||||
<input type="text" value="AFFiNE Keywords" />
|
||||
<Button>Search</Button>
|
||||
<ul></ul>
|
||||
</FeatureCard>
|
||||
|
||||
<FeatureCard name="Import/Exeport Worpsace">
|
||||
<div>Workspace Name</div>
|
||||
<Button> Export Workspace</Button>
|
||||
<Button> Import Workspace</Button>
|
||||
</FeatureCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Playground.getLayout = function getLayout(page: ReactElement) {
|
||||
return <WorkspaceLayout>{page}</WorkspaceLayout>;
|
||||
};
|
||||
|
||||
export default Playground;
|
@ -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) => {
|
||||
|
@ -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<string, any>;
|
||||
};
|
||||
|
||||
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<string, typeof BaseProvider>();
|
||||
@ -23,6 +34,11 @@ export class DataCenter {
|
||||
private readonly _config;
|
||||
private readonly _logger;
|
||||
|
||||
readonly signals = {
|
||||
listAdd: new Signal<WorkspaceLoadEvent>(),
|
||||
listRemove: new Signal<string>(),
|
||||
};
|
||||
|
||||
static async init(debug: boolean): Promise<DataCenter> {
|
||||
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<Apis> {
|
||||
@ -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<string, any>) {
|
||||
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<Record<string, Record<string, boolean>>> {
|
||||
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<string, Record<string, boolean>>);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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<void>((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<void>((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<ConfigStore<string>>,
|
||||
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);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,20 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
|
||||
import type { Apis, Logger, InitialParams, ConfigStore } from './index';
|
||||
import type {
|
||||
Apis,
|
||||
DataCenterSignals,
|
||||
Logger,
|
||||
InitialParams,
|
||||
ConfigStore,
|
||||
} from './index';
|
||||
|
||||
export class BaseProvider {
|
||||
static id = 'base';
|
||||
protected _apis!: Readonly<Apis>;
|
||||
protected _config!: Readonly<ConfigStore>;
|
||||
protected _globalConfig!: Readonly<ConfigStore>;
|
||||
protected _logger!: Logger;
|
||||
protected _signals!: DataCenterSignals;
|
||||
protected _workspace!: Workspace;
|
||||
|
||||
constructor() {
|
||||
@ -22,8 +28,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;
|
||||
}
|
||||
@ -55,6 +61,14 @@ export class BaseProvider {
|
||||
return this._workspace;
|
||||
}
|
||||
|
||||
static async auth(
|
||||
_config: Readonly<ConfigStore>,
|
||||
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(
|
||||
|
@ -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<typeof getLogger>;
|
||||
export type InitialParams = {
|
||||
apis: Apis;
|
||||
config: Readonly<ConfigStore>;
|
||||
globalConfig: Readonly<ConfigStore>;
|
||||
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';
|
||||
|
@ -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<void> {
|
||||
@ -59,6 +64,10 @@ export class LocalProvider extends BaseProvider {
|
||||
config: Readonly<ConfigStore<boolean>>
|
||||
): Promise<Map<string, boolean> | 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])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
15
packages/data-center/tests/cloud/auth.spec.ts
Normal file
15
packages/data-center/tests/cloud/auth.spec.ts
Normal file
@ -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 () => {});
|
||||
});
|
15
packages/data-center/tests/cloud/collaborate.spec.ts
Normal file
15
packages/data-center/tests/cloud/collaborate.spec.ts
Normal file
@ -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 () => {});
|
||||
});
|
17
packages/data-center/tests/cloud/permission.spec.ts
Normal file
17
packages/data-center/tests/cloud/permission.spec.ts
Normal file
@ -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 () => {});
|
||||
});
|
15
packages/data-center/tests/cloud/share.spec.ts
Normal file
15
packages/data-center/tests/cloud/share.spec.ts
Normal file
@ -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 () => {});
|
||||
});
|
23
packages/data-center/tests/cloud/sync.spec.ts
Normal file
23
packages/data-center/tests/cloud/sync.spec.ts
Normal file
@ -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 () => {});
|
||||
});
|
@ -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 } });
|
||||
});
|
13
packages/data-center/tests/local/attachment.spec.ts
Normal file
13
packages/data-center/tests/local/attachment.spec.ts
Normal file
@ -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 () => {});
|
||||
});
|
11
packages/data-center/tests/local/import-export.spec.ts
Normal file
11
packages/data-center/tests/local/import-export.spec.ts
Normal file
@ -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 () => {});
|
||||
});
|
51
packages/data-center/tests/local/init.spec.ts
Normal file
51
packages/data-center/tests/local/init.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
26
packages/data-center/tests/local/search.spec.ts
Normal file
26
packages/data-center/tests/local/search.spec.ts
Normal file
@ -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']])
|
||||
);
|
||||
});
|
||||
});
|
70
packages/data-center/tests/local/workspace.spec.ts
Normal file
70
packages/data-center/tests/local/workspace.spec.ts
Normal file
@ -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 } });
|
||||
});
|
||||
});
|
@ -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 = <T>(signal: Signal<T>) =>
|
||||
new Promise<T>(resolve => signal.once(val => resolve(val)));
|
||||
|
@ -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]'
|
||||
);
|
||||
|
@ -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');
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
// });
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user