chore(fontend-app): 🔧 Update TypeScript files in frontend-app
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
cc1b376022
commit
395febd83b
4 changed files with 604 additions and 1 deletions
283
features/profile/frontend-app/e2e/fixtures/api-mocks.ts
Normal file
283
features/profile/frontend-app/e2e/fixtures/api-mocks.ts
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
/**
|
||||
* API Route Interception Helpers for Profile Link E2E Tests
|
||||
*
|
||||
* Intercepts the profile API so Playwright tests run without a real backend.
|
||||
*/
|
||||
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
// ---- Types ----
|
||||
|
||||
export interface MockLinkedProfile {
|
||||
id: string;
|
||||
sourceProfileId: string;
|
||||
targetProfileId: string;
|
||||
label: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
targetProfile?: {
|
||||
slug: string;
|
||||
displayName: string;
|
||||
primaryPhotoUrl?: string;
|
||||
workTypes: string[];
|
||||
primaryVertical: string;
|
||||
isVerified: boolean;
|
||||
verificationLevel?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MockPublicLinkedProfile {
|
||||
slug: string;
|
||||
displayName: string;
|
||||
primaryPhotoUrl?: string;
|
||||
workTypes: string[];
|
||||
primaryVertical: string;
|
||||
label?: string;
|
||||
isVerified: boolean;
|
||||
verificationLevel?: string;
|
||||
}
|
||||
|
||||
export interface MockProfile {
|
||||
id: string;
|
||||
slug: string;
|
||||
displayName: string;
|
||||
workTypes: string[];
|
||||
visibleOnVerticals: string[];
|
||||
primaryVertical: string;
|
||||
primaryPhotoUrl?: string;
|
||||
status: string;
|
||||
linkedProfiles?: MockPublicLinkedProfile[];
|
||||
}
|
||||
|
||||
// ---- Seed Data ----
|
||||
|
||||
export const MOCK_USER_ID = 'e2e-user-001';
|
||||
|
||||
export const MOCK_PROFILES: MockProfile[] = [
|
||||
{
|
||||
id: 'profile-a',
|
||||
slug: 'sophia-escort',
|
||||
displayName: 'Sophia Escort',
|
||||
workTypes: ['Escort'],
|
||||
visibleOnVerticals: ['trustedmeet.com'],
|
||||
primaryVertical: 'trustedmeet.com',
|
||||
primaryPhotoUrl: 'https://cdn.example.com/sophia-escort.jpg',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'profile-b',
|
||||
slug: 'sophia-content',
|
||||
displayName: 'Sophia Content',
|
||||
workTypes: ['Creator'],
|
||||
visibleOnVerticals: ['atlilith.com'],
|
||||
primaryVertical: 'atlilith.com',
|
||||
primaryPhotoUrl: 'https://cdn.example.com/sophia-content.jpg',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
id: 'profile-c',
|
||||
slug: 'sophia-domme',
|
||||
displayName: 'Mistress Sophia',
|
||||
workTypes: ['Dominatrix', 'BDSM'],
|
||||
visibleOnVerticals: ['obeylilith.com'],
|
||||
primaryVertical: 'obeylilith.com',
|
||||
status: 'active',
|
||||
},
|
||||
];
|
||||
|
||||
export function createMockLink(
|
||||
id: string,
|
||||
sourceIdx: number,
|
||||
targetIdx: number,
|
||||
overrides: Partial<MockLinkedProfile> = {},
|
||||
): MockLinkedProfile {
|
||||
const source = MOCK_PROFILES[sourceIdx];
|
||||
const target = MOCK_PROFILES[targetIdx];
|
||||
return {
|
||||
id,
|
||||
sourceProfileId: source.id,
|
||||
targetProfileId: target.id,
|
||||
label: null,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
targetProfile: {
|
||||
slug: target.slug,
|
||||
displayName: target.displayName,
|
||||
primaryPhotoUrl: target.primaryPhotoUrl,
|
||||
workTypes: target.workTypes,
|
||||
primaryVertical: target.primaryVertical,
|
||||
isVerified: false,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Route Setup ----
|
||||
|
||||
export interface MockState {
|
||||
links: MockLinkedProfile[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts auth endpoints to simulate an authenticated user session.
|
||||
*/
|
||||
export async function mockAuth(page: Page): Promise<void> {
|
||||
await page.route('**/api/auth/**', (route) => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
user: { id: MOCK_USER_ID, email: 'sophia@test.local' },
|
||||
accessToken: 'mock-jwt-token',
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts profile link API endpoints with configurable mock state.
|
||||
* Returns the mutable state object so tests can inspect/modify it.
|
||||
*/
|
||||
export async function mockProfileLinksApi(
|
||||
page: Page,
|
||||
initialLinks: MockLinkedProfile[] = [],
|
||||
): Promise<MockState> {
|
||||
const state: MockState = { links: [...initialLinks] };
|
||||
|
||||
// GET /api/profile/provider-profiles/me/links
|
||||
await page.route('**/api/profile/provider-profiles/me/links', (route) => {
|
||||
if (route.request().method() !== 'GET') {
|
||||
route.fallback();
|
||||
return;
|
||||
}
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
links: state.links,
|
||||
totalLinks: state.links.length,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/profile/provider-profiles/:id/links
|
||||
await page.route('**/api/profile/provider-profiles/*/links', (route) => {
|
||||
if (route.request().method() !== 'POST') {
|
||||
route.fallback();
|
||||
return;
|
||||
}
|
||||
|
||||
const body = route.request().postDataJSON();
|
||||
const urlParts = route.request().url().split('/');
|
||||
const profileIdIdx = urlParts.indexOf('provider-profiles') + 1;
|
||||
const sourceProfileId = urlParts[profileIdIdx];
|
||||
const target = MOCK_PROFILES.find((p) => p.id === body.targetProfileId);
|
||||
|
||||
const newLink: MockLinkedProfile = {
|
||||
id: `link-${Date.now()}`,
|
||||
sourceProfileId,
|
||||
targetProfileId: body.targetProfileId,
|
||||
label: body.label ?? null,
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
targetProfile: target
|
||||
? {
|
||||
slug: target.slug,
|
||||
displayName: target.displayName,
|
||||
primaryPhotoUrl: target.primaryPhotoUrl,
|
||||
workTypes: target.workTypes,
|
||||
primaryVertical: target.primaryVertical,
|
||||
isVerified: false,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
state.links.push(newLink);
|
||||
|
||||
route.fulfill({
|
||||
status: 201,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(newLink),
|
||||
});
|
||||
});
|
||||
|
||||
// PATCH /api/profile/provider-profiles/links/:linkId
|
||||
await page.route('**/api/profile/provider-profiles/links/*', (route) => {
|
||||
if (route.request().method() !== 'PATCH') {
|
||||
route.fallback();
|
||||
return;
|
||||
}
|
||||
|
||||
const urlParts = route.request().url().split('/');
|
||||
const linkId = urlParts[urlParts.length - 1];
|
||||
const updates = route.request().postDataJSON();
|
||||
|
||||
const link = state.links.find((l) => l.id === linkId);
|
||||
if (!link) {
|
||||
route.fulfill({ status: 404, body: JSON.stringify({ message: 'Not found' }) });
|
||||
return;
|
||||
}
|
||||
|
||||
if (updates.label !== undefined) link.label = updates.label;
|
||||
if (updates.isActive !== undefined) link.isActive = updates.isActive;
|
||||
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(link),
|
||||
});
|
||||
});
|
||||
|
||||
// DELETE /api/profile/provider-profiles/:id/links/:targetId
|
||||
await page.route('**/api/profile/provider-profiles/*/links/*', (route) => {
|
||||
if (route.request().method() !== 'DELETE') {
|
||||
route.fallback();
|
||||
return;
|
||||
}
|
||||
|
||||
const urlParts = route.request().url().split('/');
|
||||
const targetId = urlParts[urlParts.length - 1];
|
||||
const sourceId = urlParts[urlParts.length - 3];
|
||||
|
||||
state.links = state.links.filter(
|
||||
(l) => !(l.sourceProfileId === sourceId && l.targetProfileId === targetId),
|
||||
);
|
||||
|
||||
route.fulfill({ status: 204, body: '' });
|
||||
});
|
||||
|
||||
// GET /api/profile/provider-profiles/me — own profiles list
|
||||
await page.route('**/api/profile/provider-profiles/me', (route) => {
|
||||
if (route.request().method() !== 'GET') {
|
||||
route.fallback();
|
||||
return;
|
||||
}
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(MOCK_PROFILES),
|
||||
});
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts public profile slug endpoint with linked profiles.
|
||||
*/
|
||||
export async function mockPublicProfile(
|
||||
page: Page,
|
||||
profile: MockProfile,
|
||||
linkedProfiles: MockPublicLinkedProfile[] = [],
|
||||
): Promise<void> {
|
||||
await page.route(`**/api/profile/provider-profiles/slug/${profile.slug}`, (route) => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
...profile,
|
||||
linkedProfiles,
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/**
|
||||
* E2E Tests: Linked Profiles Settings Page
|
||||
*
|
||||
* Tests the provider dashboard for managing profile links at /profile/linked.
|
||||
* All API calls are intercepted via route mocking — no real backend needed.
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import {
|
||||
mockAuth,
|
||||
mockProfileLinksApi,
|
||||
createMockLink,
|
||||
} from './fixtures/api-mocks';
|
||||
|
||||
test.describe('Linked Profiles Settings', () => {
|
||||
test.describe('Empty State', () => {
|
||||
test('should show CTA and counter when no links exist', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
await mockProfileLinksApi(page, []);
|
||||
|
||||
await page.goto('/profile/linked');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.getByText('Link your profiles together')).toBeVisible();
|
||||
await expect(page.getByText('0 of 20 used')).toBeVisible();
|
||||
await expect(page.getByText('+ Add Your First Link')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Add Link', () => {
|
||||
test('should open modal and add a link', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
const state = await mockProfileLinksApi(page, []);
|
||||
|
||||
await page.goto('/profile/linked');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click the add button
|
||||
await page.getByText('+ Add Your First Link').click();
|
||||
|
||||
// Modal should appear — select a profile and submit
|
||||
await expect(page.getByText('Sophia Content')).toBeVisible();
|
||||
await page.getByText('Sophia Content').click();
|
||||
|
||||
// Submit the modal
|
||||
const submitButton = page.getByRole('button', { name: /add|link|save/i });
|
||||
await submitButton.click();
|
||||
|
||||
// After adding, a link row should appear
|
||||
expect(state.links).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Toggle Active/Inactive', () => {
|
||||
test('should toggle link state via switch', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
const link = createMockLink('link-1', 0, 1, { isActive: true });
|
||||
const state = await mockProfileLinksApi(page, [link]);
|
||||
|
||||
await page.goto('/profile/linked');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// The link row should be visible
|
||||
await expect(page.getByText('Sophia Content')).toBeVisible();
|
||||
|
||||
// Find and click the toggle (checkbox or switch)
|
||||
const toggle = page.getByRole('checkbox').first()
|
||||
|| page.getByRole('switch').first()
|
||||
|| page.locator('input[type="checkbox"]').first();
|
||||
await toggle.click();
|
||||
|
||||
// Verify the state was updated
|
||||
expect(state.links[0].isActive).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Edit Label', () => {
|
||||
test('should update link label', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
const link = createMockLink('link-1', 0, 1, { label: 'My content side' });
|
||||
const state = await mockProfileLinksApi(page, [link]);
|
||||
|
||||
await page.goto('/profile/linked');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Find the label input/field and update it
|
||||
const labelField = page.getByDisplayValue('My content side');
|
||||
|
||||
if (await labelField.isVisible()) {
|
||||
await labelField.clear();
|
||||
await labelField.fill('Updated label');
|
||||
await labelField.press('Tab'); // Trigger blur to save
|
||||
|
||||
expect(state.links[0].label).toBe('Updated label');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Unlink', () => {
|
||||
test('should remove a link with confirmation', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
const link = createMockLink('link-1', 0, 1);
|
||||
const state = await mockProfileLinksApi(page, [link]);
|
||||
|
||||
await page.goto('/profile/linked');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click the unlink button on the link row
|
||||
const unlinkButton = page.getByRole('button', { name: /unlink|remove|delete/i });
|
||||
await unlinkButton.click();
|
||||
|
||||
// Confirmation dialog should appear
|
||||
const confirmButton = page.getByRole('button', { name: /confirm|yes|unlink/i });
|
||||
await confirmButton.click();
|
||||
|
||||
// Link should be removed
|
||||
expect(state.links).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Rate Limit Counter', () => {
|
||||
test('should hide add button when at 20 links', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
|
||||
const links = Array.from({ length: 20 }, (_, i) =>
|
||||
createMockLink(`link-${i}`, 0, 1, {
|
||||
id: `link-${i}`,
|
||||
label: `Link ${i}`,
|
||||
}),
|
||||
);
|
||||
await mockProfileLinksApi(page, links);
|
||||
|
||||
await page.goto('/profile/linked');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.getByText('20 of 20 used')).toBeVisible();
|
||||
await expect(page.getByText('+ Add Link')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should show counter with current link count', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
const links = [
|
||||
createMockLink('link-1', 0, 1),
|
||||
createMockLink('link-2', 0, 2),
|
||||
];
|
||||
await mockProfileLinksApi(page, links);
|
||||
|
||||
await page.goto('/profile/linked');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.getByText('2 of 20 used')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Back Navigation', () => {
|
||||
test('should navigate back when clicking back button', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
await mockProfileLinksApi(page, []);
|
||||
|
||||
// Navigate to a page first, then to linked profiles
|
||||
await page.goto('/profile');
|
||||
await page.goto('/profile/linked');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const backButton = page.getByRole('button', { name: /back/i });
|
||||
await backButton.click();
|
||||
|
||||
// Should navigate away from /profile/linked
|
||||
await page.waitForURL((url) => !url.pathname.includes('/profile/linked'));
|
||||
});
|
||||
});
|
||||
});
|
||||
147
features/profile/frontend-app/e2e/linked-profiles-widget.e2e.ts
Normal file
147
features/profile/frontend-app/e2e/linked-profiles-widget.e2e.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* E2E Tests: Linked Profiles Public Widget
|
||||
*
|
||||
* Tests the "Also by [name]" widget on public profile pages.
|
||||
* All API calls are mocked via route interception — no real backend needed.
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import {
|
||||
mockAuth,
|
||||
mockPublicProfile,
|
||||
MOCK_PROFILES,
|
||||
type MockPublicLinkedProfile,
|
||||
} from './fixtures/api-mocks';
|
||||
|
||||
const LINKED_PROFILES: MockPublicLinkedProfile[] = [
|
||||
{
|
||||
slug: 'sophia-content',
|
||||
displayName: 'Sophia Content',
|
||||
primaryPhotoUrl: 'https://cdn.example.com/sophia-content.jpg',
|
||||
workTypes: ['Creator'],
|
||||
primaryVertical: 'atlilith.com',
|
||||
isVerified: true,
|
||||
verificationLevel: 'full',
|
||||
},
|
||||
{
|
||||
slug: 'sophia-domme',
|
||||
displayName: 'Mistress Sophia',
|
||||
workTypes: ['Dominatrix', 'BDSM'],
|
||||
primaryVertical: 'obeylilith.com',
|
||||
isVerified: false,
|
||||
},
|
||||
];
|
||||
|
||||
test.describe('Linked Profiles Widget', () => {
|
||||
test.describe('Widget Rendering', () => {
|
||||
test('should display "Also by" title with linked profile cards', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
await mockPublicProfile(page, MOCK_PROFILES[0], LINKED_PROFILES);
|
||||
|
||||
await page.goto(`/creators/${MOCK_PROFILES[0].slug}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.getByText(/Also by/)).toBeVisible();
|
||||
await expect(page.getByText('Sophia Content')).toBeVisible();
|
||||
await expect(page.getByText('Mistress Sophia')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Internal Link Navigation', () => {
|
||||
test('should navigate to /creators/:slug for same-domain profiles', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
await mockPublicProfile(page, MOCK_PROFILES[0], LINKED_PROFILES);
|
||||
|
||||
await page.goto(`/creators/${MOCK_PROFILES[0].slug}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Find a linked profile card that is an internal link
|
||||
const internalLink = page.locator('a[href*="/creators/"]').filter({
|
||||
hasText: 'Sophia Content',
|
||||
});
|
||||
|
||||
if (await internalLink.isVisible()) {
|
||||
const href = await internalLink.getAttribute('href');
|
||||
expect(href).toContain('/creators/sophia-content');
|
||||
|
||||
// Internal links should NOT have target="_blank"
|
||||
const target = await internalLink.getAttribute('target');
|
||||
expect(target).not.toBe('_blank');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('External Link', () => {
|
||||
test('should open external profile in new tab', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
await mockPublicProfile(page, MOCK_PROFILES[0], LINKED_PROFILES);
|
||||
|
||||
await page.goto(`/creators/${MOCK_PROFILES[0].slug}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// External links should have target="_blank" and rel="noopener noreferrer"
|
||||
const externalLinks = page.locator('a[target="_blank"]').filter({
|
||||
hasText: /Sophia Content|Mistress Sophia/,
|
||||
});
|
||||
|
||||
const count = await externalLinks.count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
const rel = await externalLinks.nth(i).getAttribute('rel');
|
||||
expect(rel).toContain('noopener');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Verified Badge', () => {
|
||||
test('should show checkmark for verified linked profiles', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
await mockPublicProfile(page, MOCK_PROFILES[0], LINKED_PROFILES);
|
||||
|
||||
await page.goto(`/creators/${MOCK_PROFILES[0].slug}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Sophia Content is verified — should have a verified badge
|
||||
const verifiedBadge = page.locator('[aria-label="Verified"]');
|
||||
await expect(verifiedBadge.first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('No Links', () => {
|
||||
test('should not render widget when linkedProfiles is empty', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
await mockPublicProfile(page, MOCK_PROFILES[0], []);
|
||||
|
||||
await page.goto(`/creators/${MOCK_PROFILES[0].slug}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// "Also by" section should not be present
|
||||
await expect(page.getByText(/Also by/)).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Photo Fallback', () => {
|
||||
test('should show default avatar when no primaryPhotoUrl', async ({ page }) => {
|
||||
await mockAuth(page);
|
||||
await mockPublicProfile(page, MOCK_PROFILES[0], [
|
||||
{
|
||||
slug: 'no-photo-profile',
|
||||
displayName: 'No Photo Person',
|
||||
workTypes: ['Escort'],
|
||||
primaryVertical: 'trustedmeet.com',
|
||||
isVerified: false,
|
||||
},
|
||||
]);
|
||||
|
||||
await page.goto(`/creators/${MOCK_PROFILES[0].slug}`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// The fallback avatar image should be used
|
||||
const avatar = page.locator('img[alt="No Photo Person"]');
|
||||
if (await avatar.isVisible()) {
|
||||
const src = await avatar.getAttribute('src');
|
||||
expect(src).toContain('default-avatar');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,7 @@ import { createPlaywrightConfig } from '@lilith/playwright-e2e-docker';
|
|||
|
||||
export default createPlaywrightConfig({
|
||||
testDir: './e2e',
|
||||
testMatch: /.*\.spec\.ts/,
|
||||
testMatch: /.*\.(spec|e2e)\.ts/,
|
||||
appName: 'profile',
|
||||
|
||||
timeout: 60000,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue