Merge remote-tracking branch 'origin/main' into feature/tree-navigator

This commit is contained in:
Jesper Møller Jensen
2022-08-22 12:41:59 +02:00
45 changed files with 2278 additions and 1230 deletions

View File

@@ -43,6 +43,8 @@ jobs:
# retention-days: 30
- name: Report code coverage
uses: zgosalvez/github-actions-report-lcov@v1
if: always()
continue-on-error: true
with:
coverage-files: coverage/lcov.info
artifact-name: code-coverage-report

View File

@@ -0,0 +1,20 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v2

View File

@@ -0,0 +1,34 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: DevSkim
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '19 14 * * 5'
jobs:
lint:
name: DevSkim
runs-on: ubuntu-20.04
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Run DevSkim scanner
uses: microsoft/DevSkim-Action@v1
- name: Upload DevSkim scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: devskim-results.sarif

View File

@@ -1,26 +1,91 @@
import { rest } from 'msw';
import umbracoPath from '../src/core/helpers/umbraco-path';
import { StatusResponse } from '../src/core/models';
import { ProblemDetails, StatusResponse } from '../src/core/models';
import { expect, test } from '../test';
test('installer is shown', async ({ page, worker }) => {
await worker.use(
// Override the server status to be "must-install"
rest.get(umbracoPath('/server/status'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<StatusResponse>({
serverStatus: 'must-install',
test.describe('installer tests', () => {
test.beforeEach(async ({ page, worker }) => {
await worker.use(
// Override the server status to be "must-install"
rest.get(umbracoPath('/server/status'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<StatusResponse>({
serverStatus: 'must-install',
})
);
})
);
await page.goto('/install');
await page.waitForSelector('[data-test="installer"]');
});
test('installer is shown', async ({ page }) => {
await expect(page).toHaveURL('/install');
});
test.describe('test success and failure', () => {
test.beforeEach(async ({ page }) => {
await page.waitForSelector('[data-test="installer-user"]');
await page.fill('[aria-label="name"]', 'Test');
await page.fill('[aria-label="email"]', 'test@umbraco');
await page.fill('[aria-label="password"]', 'test123456');
await page.click('[name="subscribeToNewsletter"]');
// Go to the next step
await page.click('[aria-label="Next"]');
// Set telemetry
await page.waitForSelector('[data-test="installer-telemetry"]');
await page.waitForSelector('uui-slider[name="telemetryLevel"]');
// Click [aria-label="Next"]
await page.click('[aria-label="Next"]');
// Database form
await page.waitForSelector('[data-test="installer-database"]');
});
test('installer completes successfully', async ({ page }) => {
await page.click('[aria-label="Install"]');
await page.waitForSelector('umb-backoffice', { timeout: 30000 });
});
test('installer fails', async ({ page, worker }) => {
await worker.use(
// Override the server status to be "must-install"
rest.post(umbracoPath('/install/setup'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(400),
ctx.json<ProblemDetails>({
status: 400,
type: 'validation',
detail: 'Something went wrong',
errors: {
databaseName: ['The database name is required'],
},
})
);
})
);
})
);
await page.goto('/install');
await page.click('[aria-label="Install"]');
await page.waitForSelector('[data-test="installer"]');
await page.waitForSelector('[data-test="installer-error"]');
await expect(page).toHaveURL('/install');
await expect(page.locator('[data-test="error-message"]')).toHaveText('Something went wrong', {
useInnerText: true,
});
// Click reset button
await page.click('#button-reset');
await page.waitForSelector('[data-test="installer-user"]');
});
});
});

View File

@@ -0,0 +1,62 @@
import { rest } from 'msw';
import umbracoPath from '../src/core/helpers/umbraco-path';
import { ProblemDetails, StatusResponse } from '../src/core/models';
import { expect, test } from '../test';
test.describe('upgrader tests', () => {
test.beforeEach(async ({ page, worker }) => {
await worker.use(
// Override the server status to be "must-install"
rest.get(umbracoPath('/server/status'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<StatusResponse>({
serverStatus: 'must-upgrade',
})
);
})
);
await page.goto('/upgrade');
});
test('upgrader is shown', async ({ page }) => {
await page.waitForSelector('[data-test="upgrader"]');
await expect(page).toHaveURL('/upgrade');
await expect(page.locator('h1')).toHaveText('Upgrading Umbraco', { useInnerText: true });
});
test('upgrader has a "View Report" button', async ({ page }) => {
await expect(page.locator('[data-test="view-report-button"]')).toBeVisible();
});
test('upgrader completes successfully', async ({ page }) => {
await page.click('[data-test="continue-button"]');
await page.waitForSelector('umb-backoffice', { timeout: 30000 });
});
test('upgrader fails and shows error', async ({ page, worker }) => {
await worker.use(
// Override the server status to be "must-install"
rest.post(umbracoPath('/upgrade/authorize'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(400),
ctx.json<ProblemDetails>({
status: 400,
type: 'error',
detail: 'Something went wrong',
})
);
})
);
await page.click('[data-test="continue-button"]');
await expect(page.locator('[data-test="error-message"]')).toHaveText('Something went wrong', {
useInnerText: true,
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -36,9 +36,9 @@
"npm": ">=8.0.0 < 9"
},
"dependencies": {
"@umbraco-ui/uui": "^1.0.0-rc.2",
"element-internals-polyfill": "^1.1.6",
"lit": "^2.2.8",
"@umbraco-ui/uui": "^1.0.0-rc.3",
"element-internals-polyfill": "^1.1.9",
"lit": "^2.3.1",
"openapi-typescript-fetch": "^1.1.3",
"router-slot": "^1.5.5",
"rxjs": "^7.5.6",
@@ -50,7 +50,7 @@
},
"devDependencies": {
"@babel/core": "^7.18.10",
"@mdx-js/react": "^2.1.2",
"@mdx-js/react": "^2.1.3",
"@open-wc/testing": "^3.1.6",
"@playwright/test": "^1.25.0",
"@storybook/addon-a11y": "^6.5.10",
@@ -63,26 +63,26 @@
"@types/chai": "^4.3.1",
"@types/mocha": "^9.1.1",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.32.0",
"@typescript-eslint/parser": "^5.32.0",
"@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/parser": "^5.33.1",
"@web/dev-server-esbuild": "^0.3.1",
"@web/test-runner": "^0.14.0",
"@web/test-runner-playwright": "^0.8.9",
"babel-loader": "^8.2.5",
"eslint": "^8.21.0",
"eslint": "^8.22.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.4.0",
"eslint-import-resolver-typescript": "^3.4.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-lit": "^1.6.1",
"eslint-plugin-lit-a11y": "^2.2.2",
"eslint-plugin-storybook": "^0.6.4",
"lit-html": "^2.2.7",
"msw": "^0.44.2",
"lit-html": "^2.3.1",
"msw": "^0.45.0",
"msw-storybook-addon": "^1.6.3",
"playwright-msw": "^1.0.0",
"prettier": "2.7.1",
"typescript": "^4.7.4",
"vite": "^3.0.3"
"vite": "^3.0.9"
},
"msw": {
"workerDirectory": "public"

View File

@@ -99,9 +99,9 @@ const config: PlaywrightTestConfig = {
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run dev -- --mode e2e',
port: 5173,
reuseExistingServer: !process.env.CI,
command: 'npm run dev -- --port 8000 --mode e2e',
port: 8000,
reuseExistingServer: false,
},
};

View File

@@ -2,7 +2,7 @@
/* tslint:disable */
/**
* Mock Service Worker (0.44.2).
* Mock Service Worker (0.45.0).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.

View File

@@ -2,12 +2,13 @@ import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { UmbContextProviderMixin } from '../core/context';
import { UmbContextProviderMixin, UmbContextConsumerMixin } from '../core/context';
import { UmbNotificationService } from '../core/services/notification';
import { UmbModalService } from '../core/services/modal';
import { UmbDataTypeStore } from '../core/stores/data-type.store';
import { UmbDocumentTypeStore } from '../core/stores/document-type.store';
import { UmbNodeStore } from '../core/stores/node.store';
import { UmbSectionStore } from '../core/stores/section.store';
import './components/backoffice-header.element';
import './components/backoffice-main.element';
@@ -15,12 +16,13 @@ import './components/backoffice-notification-container.element';
import './components/backoffice-modal-container.element';
import './components/editor-property-layout.element';
import './components/node-property.element';
import './components/section-layout.element';
import './components/section-sidebar.element';
import './components/section-main.element';
import './sections/shared/section-layout.element';
import './sections/shared/section-sidebar.element';
import './sections/shared/section-main.element';
import { Subscription } from 'rxjs';
@defineElement('umb-backoffice')
export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) {
export default class UmbBackoffice extends UmbContextConsumerMixin(UmbContextProviderMixin(LitElement)) {
static styles = [
UUITextStyles,
css`
@@ -36,6 +38,9 @@ export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) {
`,
];
private _umbSectionStore?: UmbSectionStore;
private _currentSectionSubscription?: Subscription;
constructor() {
super();
@@ -44,6 +49,17 @@ export default class UmbBackoffice extends UmbContextProviderMixin(LitElement) {
this.provideContext('umbDocumentTypeStore', new UmbDocumentTypeStore());
this.provideContext('umbNotificationService', new UmbNotificationService());
this.provideContext('umbModalService', new UmbModalService());
// TODO: how do we want to handle context aware DI?
this.consumeContext('umbExtensionRegistry', (extensionRegistry) => {
this._umbSectionStore = new UmbSectionStore(extensionRegistry);
this.provideContext('umbSectionStore', this._umbSectionStore);
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._currentSectionSubscription?.unsubscribe();
}
render() {

View File

@@ -3,14 +3,15 @@ import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import { isPathActive, path } from 'router-slot';
import { map, Subscription } from 'rxjs';
import { Subscription } from 'rxjs';
import { getUserSections } from '../../core/api/fetcher';
import { UmbContextConsumerMixin } from '../../core/context';
import { UmbExtensionManifestSection, UmbExtensionRegistry } from '../../core/extension';
import { UmbContextConsumerMixin, UmbContextProvider, UmbContextProviderMixin } from '../../core/context';
import { UmbExtensionManifestSection } from '../../core/extension';
import { UmbSectionStore } from '../../core/stores/section.store';
import { UmbSectionContext } from '../sections/section.context';
@customElement('umb-backoffice-header-sections')
export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElement) {
export class UmbBackofficeHeaderSections extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) {
static styles: CSSResultGroup = [
UUITextStyles,
css`
@@ -40,9 +41,6 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
@state()
private _open = false;
@state()
private _allowedSection: Array<string> = [];
@state()
private _sections: Array<UmbExtensionManifestSection> = [];
@@ -55,16 +53,18 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
@state()
private _currentSectionAlias = '';
private _extensionRegistry?: UmbExtensionRegistry;
private _sectionStore?: UmbSectionStore;
private _sectionSubscription?: Subscription;
private _currentSectionSubscription?: Subscription;
constructor() {
super();
this.consumeContext('umbExtensionRegistry', (extensionRegistry: UmbExtensionRegistry) => {
this._extensionRegistry = extensionRegistry;
this.consumeContext('umbSectionStore', (sectionStore: UmbSectionStore) => {
this._sectionStore = sectionStore;
this._useSections();
this._useCurrentSection();
});
}
@@ -77,9 +77,11 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
const tab = e.currentTarget as HTMLElement;
// TODO: we need to be able to prevent the tab from setting the active state
if (tab.id === 'moreTab') {
return;
}
if (tab.id === 'moreTab') return;
if (!tab.dataset.alias) return;
this._sectionStore?.setCurrent(tab.dataset.alias);
}
private _handleLabelClick() {
@@ -89,19 +91,21 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
this._open = false;
}
private async _useSections() {
private _useSections() {
this._sectionSubscription?.unsubscribe();
const { data } = await getUserSections({});
this._allowedSection = data.sections;
this._sectionSubscription = this._sectionStore?.getAllowed().subscribe((allowedSections) => {
this._sections = allowedSections;
this._visibleSections = this._sections;
});
}
this._sectionSubscription = this._extensionRegistry
?.extensionsOfType('section')
.pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight)))
.subscribe((sections) => {
this._sections = sections.filter((section) => this._allowedSection.includes(section.alias));
this._visibleSections = this._sections;
});
private _useCurrentSection() {
this._currentSectionSubscription?.unsubscribe();
this._currentSectionSubscription = this._sectionStore?.currentAlias.subscribe((currentSectionAlias) => {
this._currentSectionAlias = currentSectionAlias;
});
}
disconnectedCallback(): void {
@@ -109,15 +113,17 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
this._sectionSubscription?.unsubscribe();
}
render() {
private _renderSections() {
return html`
<uui-tab-group id="tabs">
${this._visibleSections.map(
(section: UmbExtensionManifestSection) => html`
<uui-tab
?active="${isPathActive(`/section/${section.meta.pathname}`, path())}"
@click="${this._handleTabClick}"
?active="${this._currentSectionAlias === section.alias}"
href="${`/section/${section.meta.pathname}`}"
label="${section.name}"></uui-tab>
label="${section.name}"
data-alias="${section.alias}"></uui-tab>
`
)}
${this._renderExtraSections()}
@@ -150,6 +156,10 @@ export class UmbBackofficeHeaderSections extends UmbContextConsumerMixin(LitElem
`
);
}
render() {
return html` ${this._renderSections()} `;
}
}
declare global {

View File

@@ -2,13 +2,16 @@ import { defineElement } from '@umbraco-ui/uui-base/lib/registration';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { state } from 'lit/decorators.js';
import { map, Subscription } from 'rxjs';
import { IRoutingInfo } from 'router-slot';
import { Subscription } from 'rxjs';
import { UmbContextConsumerMixin } from '../../core/context';
import { createExtensionElement, UmbExtensionManifestSection, UmbExtensionRegistry } from '../../core/extension';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '../../core/context';
import { createExtensionElement, UmbExtensionManifestSection } from '../../core/extension';
import { UmbSectionStore } from '../../core/stores/section.store';
import { UmbSectionContext } from '../sections/section.context';
@defineElement('umb-backoffice-main')
export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) {
export class UmbBackofficeMain extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) {
static styles = [
UUITextStyles,
css`
@@ -28,40 +31,61 @@ export class UmbBackofficeMain extends UmbContextConsumerMixin(LitElement) {
@state()
private _sections: Array<UmbExtensionManifestSection> = [];
private _extensionRegistry?: UmbExtensionRegistry;
private _routePrefix = 'section/';
private _sectionContext?: UmbSectionContext;
private _sectionStore?: UmbSectionStore;
private _sectionSubscription?: Subscription;
constructor() {
super();
this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => {
this._extensionRegistry = _instance;
this.consumeContext('umbSectionStore', (_instance: UmbSectionStore) => {
this._sectionStore = _instance;
this._useSections();
});
}
private _useSections() {
private async _useSections() {
this._sectionSubscription?.unsubscribe();
this._sectionSubscription = this._extensionRegistry
?.extensionsOfType('section')
.pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight)))
.subscribe((sections) => {
this._routes = [];
this._sections = sections as Array<UmbExtensionManifestSection>;
this._sectionSubscription = this._sectionStore?.getAllowed().subscribe((sections) => {
if (!sections) return;
this._sections = sections;
this._createRoutes();
});
}
this._routes = this._sections.map((section) => {
return {
path: 'section/' + section.meta.pathname,
component: () => createExtensionElement(section),
};
});
private _createRoutes() {
this._routes = [];
this._routes = this._sections.map((section) => {
return {
path: this._routePrefix + section.meta.pathname,
component: () => createExtensionElement(section),
setup: this._onRouteSetup,
};
});
this._routes.push({
path: '**',
redirectTo: 'section/' + this._sections[0].meta.pathname,
});
});
this._routes.push({
path: '**',
redirectTo: this._routePrefix + this._sections?.[0]?.meta.pathname,
});
}
private _onRouteSetup = (_component: HTMLElement, info: IRoutingInfo) => {
const currentPath = info.match.route.path;
const section = this._sections.find((s) => this._routePrefix + s.meta.pathname === currentPath);
if (!section) return;
this._sectionStore?.setCurrent(section.alias);
this._provideSectionContext(section);
};
private _provideSectionContext(section: UmbExtensionManifestSection) {
if (!this._sectionContext) {
this._sectionContext = new UmbSectionContext(section);
this.provideContext('umbSectionContext', this._sectionContext);
} else {
this._sectionContext.update(section);
}
}
disconnectedCallback(): void {

View File

@@ -1,116 +0,0 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { IRoutingInfo } from 'router-slot';
import { map, Subscription } from 'rxjs';
import { UmbContextConsumerMixin } from '../../core/context';
import { createExtensionElement, UmbExtensionManifestDashboard, UmbExtensionRegistry } from '../../core/extension';
@customElement('umb-section-dashboards')
export class UmbSectionDashboards extends UmbContextConsumerMixin(LitElement) {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
}
#tabs {
background-color: var(--uui-color-surface);
height: 70px;
}
#router-slot {
width: 100%;
box-sizing: border-box;
padding: var(--uui-size-space-5);
display: block;
}
`,
];
@state()
private _dashboards: Array<UmbExtensionManifestDashboard> = [];
@state()
private _current = '';
@state()
private _routes: Array<any> = [];
private _extensionRegistry?: UmbExtensionRegistry;
private _dashboardsSubscription?: Subscription;
constructor() {
super();
this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => {
this._extensionRegistry = _instance;
this._useDashboards();
});
}
private _useDashboards() {
this._dashboardsSubscription?.unsubscribe();
this._dashboardsSubscription = this._extensionRegistry
?.extensionsOfType('dashboard')
.pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight)))
.subscribe((dashboards) => {
this._dashboards = dashboards;
this._routes = [];
this._routes = this._dashboards.map((dashboard) => {
return {
path: `${dashboard.meta.pathname}`,
component: () => createExtensionElement(dashboard),
setup: (_element: UmbExtensionManifestDashboard, info: IRoutingInfo) => {
this._current = info.match.route.path;
},
};
});
this._routes.push({
path: '**',
redirectTo: this._dashboards[0].meta.pathname,
});
});
}
private _handleTabClick(e: PointerEvent, dashboard: UmbExtensionManifestDashboard) {
// TODO: generate URL from context/location. Or use Router-link concept?
history.pushState(null, '', `/section/content/dashboard/${dashboard.meta.pathname}`);
this._current = dashboard.name;
}
disconnectedCallback() {
super.disconnectedCallback();
this._dashboardsSubscription?.unsubscribe();
}
render() {
return html`
<uui-tab-group id="tabs">
${this._dashboards.map(
(dashboard: UmbExtensionManifestDashboard) => html`
<uui-tab
label=${dashboard.name}
?active="${dashboard.meta.pathname === this._current}"
@click="${(e: PointerEvent) => this._handleTabClick(e, dashboard)}"></uui-tab>
`
)}
</uui-tab-group>
<router-slot id="router-slot" .routes="${this._routes}"></router-slot>
`;
}
}
export default UmbSectionDashboards;
declare global {
interface HTMLElementTagNameMap {
'umb-section-dashboards': UmbSectionDashboards;
}
}

View File

@@ -0,0 +1,22 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('umb-dashboard-examine-management')
export class UmbDashboardExamineManagementElement extends LitElement {
static styles = [UUITextStyles, css``];
render() {
return html`
<uui-box>
<h1>Examine Management</h1>
</uui-box>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-dashboard-examine-management': UmbDashboardExamineManagementElement;
}
}

View File

@@ -0,0 +1,22 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('umb-dashboard-media-management')
export class UmbDashboardMediaManagementElement extends LitElement {
static styles = [UUITextStyles, css``];
render() {
return html`
<uui-box>
<h1>Media Management</h1>
</uui-box>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-dashboard-media-management': UmbDashboardMediaManagementElement;
}
}

View File

@@ -0,0 +1,22 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('umb-dashboard-models-builder')
export class UmbDashboardModelsBuilderElement extends LitElement {
static styles = [UUITextStyles, css``];
render() {
return html`
<uui-box>
<h1>Models Builder</h1>
</uui-box>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-dashboard-models-builder': UmbDashboardModelsBuilderElement;
}
}

View File

@@ -0,0 +1,22 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('umb-dashboard-settings-about')
export class UmbDashboardSettingsAboutElement extends LitElement {
static styles = [UUITextStyles, css``];
render() {
return html`
<uui-box>
<h1>Settings</h1>
</uui-box>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-dashboard-settings-about': UmbDashboardSettingsAboutElement;
}
}

View File

@@ -23,7 +23,7 @@ export class UmbContentSection extends LitElement {
private _routes: Array<IRoute> = [
{
path: 'dashboard',
component: () => import('../../components/section-dashboards.element'),
component: () => import('../shared/section-dashboards.element'),
setup: () => {
this._currentNodeId = undefined;
},

View File

@@ -23,7 +23,7 @@ export class UmbMediaSection extends LitElement {
private _routes: Array<IRoute> = [
{
path: 'dashboard',
component: () => import('../../components/section-dashboards.element'),
component: () => import('../shared/section-dashboards.element'),
setup: () => {
this._currentNodeId = undefined;
},

View File

@@ -0,0 +1,30 @@
import { BehaviorSubject, Observable } from 'rxjs';
import { UmbExtensionManifestSection } from '../../core/extension';
export class UmbSectionContext {
// TODO: figure out how fine grained we want to make our observables.
private _data: BehaviorSubject<UmbExtensionManifestSection> = new BehaviorSubject({
type: 'section',
alias: '',
name: '',
meta: {
pathname: '',
weight: 0,
},
});
public readonly data: Observable<UmbExtensionManifestSection> = this._data.asObservable();
constructor(section: UmbExtensionManifestSection) {
if (!section) return;
this._data.next(section);
}
// TODO: figure out how we want to update data
public update(data: Partial<UmbExtensionManifestSection>) {
this._data.next({ ...this._data.getValue(), ...data });
}
public getData() {
return this._data.getValue();
}
}

View File

@@ -11,7 +11,7 @@ export class UmbSettingsSection extends UmbContextConsumerMixin(LitElement) {
private _routes: Array<IRoute> = [
{
path: 'dashboard',
component: () => import('../../components/section-dashboards.element'),
component: () => import('../shared/section-dashboards.element'),
},
{
path: 'extensions',

View File

@@ -0,0 +1,158 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { IRoutingInfo } from 'router-slot';
import { map, Subscription, first } from 'rxjs';
import { UmbContextConsumerMixin } from '../../../core/context';
import { createExtensionElement, UmbExtensionManifestDashboard, UmbExtensionRegistry } from '../../../core/extension';
import { UmbSectionContext } from '../section.context';
@customElement('umb-section-dashboards')
export class UmbSectionDashboards extends UmbContextConsumerMixin(LitElement) {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
}
#tabs {
background-color: var(--uui-color-surface);
height: 70px;
}
#router-slot {
width: 100%;
box-sizing: border-box;
padding: var(--uui-size-space-5);
display: block;
}
`,
];
@state()
private _dashboards: Array<UmbExtensionManifestDashboard> = [];
@state()
private _currentDashboardPathname = '';
@state()
private _routes: Array<any> = [];
@state()
private _currentSectionPathname = '';
private _currentSectionAlias = '';
private _extensionRegistry?: UmbExtensionRegistry;
private _dashboardsSubscription?: Subscription;
private _sectionContext?: UmbSectionContext;
private _sectionContextSubscription?: Subscription;
constructor() {
super();
// TODO: wait for more contexts
this.consumeContext('umbExtensionRegistry', (_instance: UmbExtensionRegistry) => {
this._extensionRegistry = _instance;
});
this.consumeContext('umbSectionContext', (context: UmbSectionContext) => {
this._sectionContext = context;
this._useSectionContext();
});
}
private _useSectionContext() {
this._sectionContextSubscription?.unsubscribe();
this._sectionContextSubscription = this._sectionContext?.data.pipe(first()).subscribe((section) => {
this._currentSectionAlias = section.alias;
this._currentSectionPathname = section.meta.pathname;
this._useDashboards();
});
}
private _useDashboards() {
if (!this._extensionRegistry || !this._currentSectionAlias) return;
this._dashboardsSubscription?.unsubscribe();
this._dashboardsSubscription = this._extensionRegistry
?.extensionsOfType('dashboard')
.pipe(
map((extensions) =>
extensions
.filter((extension) => extension.meta.sections.includes(this._currentSectionAlias))
.sort((a, b) => b.meta.weight - a.meta.weight)
)
)
.subscribe((dashboards) => {
if (dashboards?.length === 0) return;
this._dashboards = dashboards;
this._createRoutes();
});
}
private _createRoutes() {
this._routes = [];
this._routes = this._dashboards.map((dashboard) => {
return {
path: `${dashboard.meta.pathname}`,
component: () => createExtensionElement(dashboard),
setup: (_element: UmbExtensionManifestDashboard, info: IRoutingInfo) => {
this._currentDashboardPathname = info.match.route.path;
},
};
});
this._routes.push({
path: '**',
redirectTo: this._dashboards?.[0]?.meta.pathname,
});
}
private _renderNavigation() {
return html`
${this._dashboards?.length > 1
? html`
<uui-tab-group id="tabs">
${this._dashboards.map(
(dashboard: UmbExtensionManifestDashboard) => html`
<uui-tab
href="${`/section/${this._currentSectionPathname}/dashboard/${dashboard.meta.pathname}`}"
label=${dashboard.meta.label || dashboard.name}
?active="${dashboard.meta.pathname === this._currentDashboardPathname}"></uui-tab>
`
)}
</uui-tab-group>
`
: nothing}
`;
}
disconnectedCallback() {
super.disconnectedCallback();
this._dashboardsSubscription?.unsubscribe();
this._sectionContextSubscription?.unsubscribe();
}
render() {
return html`
${this._renderNavigation()}
<router-slot id="router-slot" .routes="${this._routes}"></router-slot>
`;
}
}
export default UmbSectionDashboards;
declare global {
interface HTMLElementTagNameMap {
'umb-section-dashboards': UmbSectionDashboards;
}
}

View File

@@ -51,6 +51,7 @@ export type UmbManifestPropertyActionMeta = {
// Dashboard:
export type UmbManifestDashboardMeta = {
sections: Array<string>;
label?: string;
pathname: string; // TODO: how to we want to support pretty urls?
weight: number;
};

View File

@@ -0,0 +1,30 @@
import { map, Observable, ReplaySubject } from 'rxjs';
import { UmbExtensionRegistry } from '../extension';
export class UmbSectionStore {
private _extensionRegistry: UmbExtensionRegistry;
private _currentAlias: ReplaySubject<string> = new ReplaySubject(1);
public readonly currentAlias: Observable<string> = this._currentAlias.asObservable();
// TODO: how do we want to handle DI in contexts?
constructor(extensionRegistry: UmbExtensionRegistry) {
this._extensionRegistry = extensionRegistry;
}
public getAllowed() {
// TODO: implemented allowed filtering
/*
const { data } = await getUserSections({});
this._allowedSection = data.sections;
*/
return this._extensionRegistry
?.extensionsOfType('section')
.pipe(map((extensions) => extensions.sort((a, b) => b.meta.weight - a.meta.weight)));
}
public setCurrent(alias: string) {
this._currentAlias.next(alias);
}
}

View File

@@ -1,4 +1,4 @@
@import '@umbraco-ui/uui-css/dist/uui-css.css';
@import '../../node_modules/@umbraco-ui/uui-css/dist/uui-css.css';
:root {
--uui-color-positive: #1c874c;

View File

@@ -3,4 +3,5 @@ export * from './installer-database.element';
export * from './installer-installing.element';
export * from './installer-user.element';
export * from './installer-layout.element';
export * from './installer-error.element';
export * from './installer.element';

View File

@@ -125,7 +125,7 @@ export class UmbInstallerConsent extends UmbContextConsumerMixin(LitElement) {
render() {
return html`
<div id="container" class="uui-text">
<div id="container" class="uui-text" data-test="installer-telemetry">
<h1>Consent for telemetry data</h1>
${this._renderSlider()}
<div id="buttons">

View File

@@ -4,11 +4,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { UmbContextConsumerMixin } from '../core/context';
import {
ProblemDetails,
UmbracoInstallerDatabaseModel,
UmbracoPerformInstallDatabaseConfiguration,
} from '../core/models';
import { UmbracoInstallerDatabaseModel, UmbracoPerformInstallDatabaseConfiguration } from '../core/models';
import { UmbInstallerContext } from './installer-context';
@customElement('umb-installer-database')
@@ -73,19 +69,12 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
margin-left: auto;
min-width: 120px;
}
#error-message {
color: var(--uui-color-error, red);
}
`,
];
@query('#button-install')
private _installButton!: UUIButtonElement;
@query('#error-message')
private _errorMessage!: HTMLElement;
@property({ attribute: false })
public databaseFormData!: UmbracoPerformInstallDatabaseConfiguration;
@@ -179,17 +168,10 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
this._installerStore?.appendData({ database });
}
this._installerStore?.requestInstall().then(this._handleFulfilled.bind(this), this._handleRejected.bind(this));
this.dispatchEvent(new CustomEvent('submit', { bubbles: true, composed: true }));
this._installButton.state = 'waiting';
};
private _handleFulfilled() {
this.dispatchEvent(new CustomEvent('next', { bubbles: true, composed: true }));
this._installButton.state = undefined;
}
private _handleRejected(error: ProblemDetails) {
this._installButton.state = 'failed';
this._errorMessage.innerText = error.type;
}
private _onBack() {
this.dispatchEvent(new CustomEvent('previous', { bubbles: true, composed: true }));
@@ -197,7 +179,6 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
private get selectedDatabase() {
const id = this._installerStore?.getData().database?.id;
console.log('selected id', id, this._databases);
return this._databases.find((x) => x.id === id) ?? this._databases[0];
}
@@ -333,7 +314,7 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
`;
render() {
return html` <div id="container" class="uui-text">
return html` <div id="container" class="uui-text" data-test="installer-database">
<h1 class="uui-h3">Database Configuration</h1>
<uui-form>
<form id="database-form" name="database" @submit="${this._handleSubmit}">
@@ -341,9 +322,6 @@ export class UmbInstallerDatabase extends UmbContextConsumerMixin(LitElement) {
? this._renderPreConfiguredDatabase(this._preConfiguredDatabase)
: this._renderDatabaseSelection()}
<!-- TODO: Apply error message to the fields that has errors -->
<p id="error-message"></p>
<div id="buttons">
<uui-button label="Back" @click=${this._onBack} look="secondary"></uui-button>
<uui-button id="button-install" type="submit" label="Install" look="primary" color="positive"></uui-button>

View File

@@ -0,0 +1,76 @@
import { css, CSSResultGroup, html, LitElement, nothing } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ProblemDetails } from '../core/models';
@customElement('umb-installer-error')
export class UmbInstallerError extends LitElement {
static styles: CSSResultGroup = [
css`
:host,
#container {
display: flex;
flex-direction: column;
height: 100%;
}
h1 {
text-align: center;
}
#error-message {
color: var(--uui-color-error, red);
}
`,
];
@property({ type: Object })
error?: ProblemDetails;
private _handleSubmit(e: SubmitEvent) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('reset', { bubbles: true, composed: true }));
}
private _renderError(error: ProblemDetails) {
return html`
<p id="error-message" data-test="error-message">${error.detail ?? 'Unknown error'}</p>
<hr />
${error.errors ? this._renderErrors(error.errors) : nothing}
`;
}
private _renderErrors(errors: Record<string, unknown>) {
return html`
<ul>
${Object.keys(errors).map((key) => html` <li>${key}: ${(errors[key] as string[]).join(', ')}</li> `)}
</ul>
`;
}
render() {
return html` <div id="container" class="uui-text" data-test="installer-error">
<uui-form>
<form id="installer-form" @submit="${this._handleSubmit}">
<h1 class="uui-h3">Installing Umbraco</h1>
<h2>Something went wrong</h2>
${this.error ? this._renderError(this.error) : nothing}
<div id="buttons">
<uui-button
id="button-reset"
type="submit"
label="Go back and try again"
look="primary"
color="positive"></uui-button>
</div>
</form>
</uui-form>
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-installer-error': UmbInstallerError;
}
}

View File

@@ -1,5 +1,6 @@
import { css, CSSResultGroup, html, LitElement, PropertyValueMap } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('umb-installer-installing')
export class UmbInstallerInstalling extends LitElement {
static styles: CSSResultGroup = [
@@ -10,32 +11,10 @@ export class UmbInstallerInstalling extends LitElement {
`,
];
@state()
private _installProgress = 0;
protected firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.firstUpdated(_changedProperties);
this._updateProgress();
}
private async _updateProgress() {
this._installProgress = Math.min(this._installProgress + (Math.random() + 1) * 10, 100);
await new Promise((resolve) => setTimeout(resolve, (Math.random() + 1) * 1000));
if (this._installProgress >= 100) {
// Redirect to backoffice
history.replaceState(null, '', '/');
return;
}
this._updateProgress();
}
render() {
return html` <div class="uui-text">
return html` <div class="uui-text" data-test="installer-installing">
<h1 class="uui-h3">Installing Umbraco</h1>
<uui-progress-bar progress=${this._installProgress}></uui-progress-bar>
<uui-loader-bar></uui-loader-bar>
</div>`;
}
}

View File

@@ -107,7 +107,7 @@ export class UmbInstallerUser extends UmbContextConsumerMixin(LitElement) {
};
render() {
return html` <div id="container" class="uui-text">
return html` <div id="container" class="uui-text" data-test="installer-user">
<h1>Install Umbraco</h1>
<uui-form>
<form id="LoginForm" name="login" @submit="${this._handleSubmit}">

View File

@@ -1,5 +1,6 @@
import './installer-consent.element';
import './installer-database.element';
import './installer-error.element';
import './installer-installing.element';
import './installer-layout.element';
import './installer-user.element';
@@ -7,7 +8,9 @@ import './installer-user.element';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { postInstallSetup } from '../core/api/fetcher';
import { UmbContextProviderMixin } from '../core/context';
import { ProblemDetails } from '../core/models';
import { UmbInstallerContext } from './installer-context';
@customElement('umb-installer')
@@ -17,15 +20,21 @@ export class UmbInstaller extends UmbContextProviderMixin(LitElement) {
@state()
step = 1;
private _umbInstallerContext = new UmbInstallerContext();
private _error?: ProblemDetails;
constructor() {
super();
this.provideContext('umbInstallerContext', new UmbInstallerContext());
this.provideContext('umbInstallerContext', this._umbInstallerContext);
}
connectedCallback(): void {
super.connectedCallback();
this.addEventListener('next', () => this._handleNext());
this.addEventListener('previous', () => this._goToPreviousStep());
this.addEventListener('submit', () => this._handleSubmit());
this.addEventListener('reset', () => this._handleReset());
}
private _handleNext() {
@@ -36,6 +45,36 @@ export class UmbInstaller extends UmbContextProviderMixin(LitElement) {
this.step--;
}
private _handleFulfilled() {
console.warn('TODO: Set up real authentication');
sessionStorage.setItem('is-authenticated', 'true');
history.replaceState(null, '', '/content');
}
private _handleRejected(e: unknown) {
if (e instanceof postInstallSetup.Error) {
const error = e.getActualType();
if (error.status === 400) {
this._error = error.data;
}
}
this._handleNext();
}
private _handleSubmit() {
this._handleNext();
this._umbInstallerContext
.requestInstall()
.then(() => this._handleFulfilled())
.catch((error) => this._handleRejected(error));
}
private _handleReset() {
this.step = 1;
this._error = undefined;
}
private _renderSection() {
switch (this.step) {
case 2:
@@ -44,6 +83,8 @@ export class UmbInstaller extends UmbContextProviderMixin(LitElement) {
return html`<umb-installer-database></umb-installer-database>`;
case 4:
return html`<umb-installer-installing></umb-installer-installing>`;
case 5:
return html`<umb-installer-error .error=${this._error}></umb-installer-error>`;
default:
return html`<umb-installer-user></umb-installer-user>`;

View File

@@ -1,6 +1,7 @@
import '../core/context/context-provider.element';
import './installer-consent.element';
import './installer-database.element';
import './installer-error.element';
import './installer-installing.element';
import './installer-user.element';
@@ -8,10 +9,10 @@ import { Meta, Story } from '@storybook/web-components';
import { html } from 'lit-html';
import { rest } from 'msw';
import { UmbInstallerUser } from '.';
import { UmbracoInstaller } from '../core/models';
import { UmbInstallerContext } from './installer-context';
import type { UmbInstallerError, UmbInstallerUser } from '.';
import type { UmbracoInstaller } from '../core/models';
export default {
title: 'Components/Installer/Steps',
component: 'umb-installer',
@@ -91,3 +92,27 @@ Step3DatabasePreconfigured.parameters = {
export const Step4Installing: Story = () => html`<umb-installer-installing></umb-installer-installing>`;
Step4Installing.storyName = 'Step 4: Installing';
export const Step5Error: Story<UmbInstallerError> = ({ error }) =>
html`<umb-installer-error .error=${error}></umb-installer-error>`;
Step5Error.storyName = 'Step 5: Error';
Step5Error.args = {
error: {
type: 'validation',
status: 400,
detail: 'The form did not pass validation',
title: 'Validation error',
errors: {
'user.password': [
'The password must be at least 6 characters long',
'The password must contain at least one number',
],
databaseName: ['The database name is required'],
},
},
};
Step5Error.parameters = {
actions: {
handles: ['reset'],
},
};

View File

@@ -73,16 +73,18 @@ export const handlers = [
);
}),
rest.post<PostInstallRequest>(umbracoPath('/install/settings'), async (req, res, ctx) => {
rest.post(umbracoPath('/install/setup'), async (req, res, ctx) => {
await new Promise((resolve) => setTimeout(resolve, (Math.random() + 1) * 1000)); // simulate a delay of 1-2 seconds
const body = await req.json<PostInstallRequest>();
if (req.body.database?.name === 'fail') {
if (body.database?.name === 'fail') {
return res(
// Respond with a 200 status code
ctx.status(400),
ctx.json<ProblemDetails>({
type: 'validation',
status: 400,
detail: 'Something went wrong',
errors: {
name: ['Database name is invalid'],
},

View File

@@ -51,7 +51,7 @@ export const internalManifests: Array<UmbExtensionManifestCore> = [
alias: 'Umb.Dashboard.Welcome',
name: 'Welcome',
elementName: 'umb-dashboard-welcome',
js: () => import('./backoffice/dashboards/dashboard-welcome.element'),
js: () => import('./backoffice/dashboards/welcome/dashboard-welcome.element'),
meta: {
sections: ['Umb.Section.Content'],
pathname: 'welcome', // TODO: how to we want to support pretty urls?
@@ -63,13 +63,62 @@ export const internalManifests: Array<UmbExtensionManifestCore> = [
alias: 'Umb.Dashboard.RedirectManagement',
name: 'Redirect Management',
elementName: 'umb-dashboard-redirect-management',
js: () => import('./backoffice/dashboards/dashboard-redirect-management.element'),
js: () => import('./backoffice/dashboards/redirect-management/dashboard-redirect-management.element'),
meta: {
sections: ['Umb.Section.Content'],
pathname: 'redirect-management', // TODO: how to we want to support pretty urls?
weight: 10,
},
},
{
type: 'dashboard',
alias: 'Umb.Dashboard.SettingsAbout',
name: 'Settings About',
elementName: 'umb-dashboard-settings-about',
js: () => import('./backoffice/dashboards/settings-about/dashboard-settings-about.element'),
meta: {
label: 'About',
sections: ['Umb.Section.Settings'],
pathname: 'about', // TODO: how to we want to support pretty urls?
weight: 10,
},
},
{
type: 'dashboard',
alias: 'Umb.Dashboard.ExamineManagement',
name: 'Examine Management',
elementName: 'umb-dashboard-examine-management',
js: () => import('./backoffice/dashboards/examine-management/dashboard-examine-management.element'),
meta: {
sections: ['Umb.Section.Settings'],
pathname: 'examine-management', // TODO: how to we want to support pretty urls?
weight: 10,
},
},
{
type: 'dashboard',
alias: 'Umb.Dashboard.ModelsBuilder',
name: 'Models Builder',
elementName: 'umb-dashboard-models-builder',
js: () => import('./backoffice/dashboards/models-builder/dashboard-models-builder.element'),
meta: {
sections: ['Umb.Section.Settings'],
pathname: 'models-builder', // TODO: how to we want to support pretty urls?
weight: 10,
},
},
{
type: 'dashboard',
alias: 'Umb.Dashboard.MediaManagement',
name: 'Media',
elementName: 'umb-dashboard-media-management',
js: () => import('./backoffice/dashboards/media-management/dashboard-media-management.element'),
meta: {
sections: ['Umb.Section.Media'],
pathname: 'media-management', // TODO: how to we want to support pretty urls?
weight: 10,
},
},
{
type: 'propertyEditorUI',
alias: 'Umb.PropertyEditorUI.Text',

View File

@@ -55,6 +55,7 @@ export class UmbUpgraderView extends LitElement {
? html`
<p>
<uui-button
data-test="view-report-button"
look="secondary"
href="${this.settings.reportUrl}"
target="_blank"
@@ -69,6 +70,7 @@ export class UmbUpgraderView extends LitElement {
<form id="authorizeUpgradeForm" @submit=${this._handleSubmit}>
<p>
<uui-button
data-test="continue-button"
id="authorizeUpgrade"
type="submit"
look="primary"
@@ -84,7 +86,9 @@ export class UmbUpgraderView extends LitElement {
}
private _renderError() {
return html` ${this.errorMessage ? html`<p class="error">${this.errorMessage}</p>` : ''} `;
return html`
${this.errorMessage ? html`<p class="error" data-test="error-message">${this.errorMessage}</p>` : ''}
`;
}
render() {

View File

@@ -5,7 +5,8 @@ import { html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { getUpgradeSettings, PostUpgradeAuthorize } from '../core/api/fetcher';
import { UmbracoUpgrader } from '../core/models';
import type { UmbracoUpgrader } from '../core/models';
/**
* @element umb-upgrader
@@ -30,7 +31,7 @@ export class UmbUpgrader extends LitElement {
}
render() {
return html`<umb-installer-layout>
return html`<umb-installer-layout data-test="upgrader">
<umb-upgrader-view
.fetching=${this.fetching}
.upgrading=${this.upgrading}
@@ -49,7 +50,7 @@ export class UmbUpgrader extends LitElement {
this.upgradeSettings = data;
} catch (e) {
if (e instanceof getUpgradeSettings.Error) {
this.errorMessage = e.message;
this.errorMessage = e.data.detail;
}
}

View File

@@ -4,10 +4,18 @@ import { createWorkerFixture } from 'playwright-msw';
import { handlers } from './src/mocks/e2e-handlers';
import type { MockServiceWorker } from 'playwright-msw';
const test = base.extend<{
worker: MockServiceWorker;
}>({
worker: createWorkerFixture(...handlers),
page: async ({ page }, use) => {
// Set is-authenticated in sessionStorage to true
await page.addInitScript(`window.sessionStorage.setItem('is-authenticated', 'true');`);
// Use signed-in page in all tests
await use(page);
},
});
export { test, expect };