Skip to content

Commit b29353b

Browse files
committed
feat: add screenshots check
1 parent 047e1c0 commit b29353b

File tree

55 files changed

+736
-359
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+736
-359
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"test:watch": "cross-env POLKADOTJS_DISABLE_ESM_CJS_WARNING=1 vitest",
4949
"test:coverage": "cross-env POLKADOTJS_DISABLE_ESM_CJS_WARNING=1 vitest run --coverage",
5050
"test:coverage-new-files": "cross-env POLKADOTJS_DISABLE_ESM_CJS_WARNING=1 vitest run --coverage --changed dev",
51+
"test:screenshots": "playwright test --config=playwright.screenshots.config.ts",
5152
"test:system": "cross-env CHAINS_FILE=chains_dev playwright test",
5253
"test:system:regress": "cross-env CHAINS_FILE=chains_dev playwright test --grep @regress",
5354
"test:system:load-fee": "cross-env CHAINS_FILE=chains_dev playwright test --grep @fee-test",

playwright.screenshots.config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
export default defineConfig({
4+
testDir: './tests/screenshots',
5+
fullyParallel: false,
6+
retries: 0,
7+
workers: 1,
8+
reporter: 'list',
9+
use: {
10+
baseURL: 'http://localhost:6006',
11+
...devices['Desktop Chrome'],
12+
},
13+
timeout: 30000,
14+
});

src/renderer/domains/network/account/resource.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { attach, combine, createStore } from 'effector';
22

3-
import { type Chain, type Wallet } from '@/shared/core';
3+
import { type Chain, type Contact, type Wallet } from '@/shared/core';
44
import { type AccountId } from '@/shared/polkadotjs-schemas';
55
import { createQueryResource } from '@/shared/query';
66
import { contactModel } from '@/entities/contact';
@@ -28,7 +28,16 @@ export type AccountsNameParams = {
2828

2929
type NameCache = Record<string, string>;
3030

31-
const getContactsStore = () => contactModel?.$contacts ?? createStore([]);
31+
const $contactsFallback = createStore<Contact[]>([]);
32+
33+
// Defensive: contactModel may be in TDZ during circular module initialization (e.g. Storybook)
34+
const getContactsStore = () => {
35+
try {
36+
return contactModel.$contacts;
37+
} catch {
38+
return $contactsFallback;
39+
}
40+
};
3241

3342
const getNameResolverSource = () => {
3443
return combine({

src/renderer/entities/contact/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export {
22
BackendContactRow,
33
BackendErrorView,
44
BackendLoadingView,
5+
CachedWithErrorView,
56
ContactList,
67
ContactRow,
78
ContactSkeleton,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { type Meta, type StoryObj } from '@storybook/react-vite';
2+
3+
import { type BackendContact } from '@/shared/core';
4+
import { TEST_ACCOUNTS, TEST_ADDRESS } from '@/shared/lib/utils';
5+
6+
import { BackendContactRow } from './BackendContactRow';
7+
8+
const mockContact: BackendContact = {
9+
id: '1',
10+
name: 'Treasury Multisig',
11+
address: TEST_ADDRESS,
12+
accountId: TEST_ACCOUNTS[0],
13+
source: 'backend',
14+
entityNames: ['Nova Foundation', 'Treasury'],
15+
chainId: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
16+
chainName: 'Polkadot',
17+
categoryName: 'Infrastructure',
18+
contactTypeName: 'Multisig',
19+
derivationPath: null,
20+
ownerAccountId: null,
21+
};
22+
23+
const meta: Meta<typeof BackendContactRow> = {
24+
title: 'Address Book/BackendContactRow',
25+
component: BackendContactRow,
26+
decorators: [
27+
(Story) => (
28+
<ul>
29+
<Story />
30+
</ul>
31+
),
32+
],
33+
args: {
34+
contact: mockContact,
35+
onSendTo: () => {},
36+
},
37+
};
38+
39+
export default meta;
40+
41+
type Story = StoryObj<typeof BackendContactRow>;
42+
43+
export const Default: Story = {};
44+
45+
export const MinimalLabels: Story = {
46+
args: {
47+
contact: {
48+
...mockContact,
49+
id: '2',
50+
name: 'Simple Contact',
51+
contactTypeName: null,
52+
entityNames: [],
53+
},
54+
},
55+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { type Meta, type StoryObj } from '@storybook/react-vite';
2+
3+
import { BackendErrorView } from './BackendErrorView';
4+
5+
const meta: Meta<typeof BackendErrorView> = {
6+
title: 'Address Book/BackendErrorView',
7+
component: BackendErrorView,
8+
args: {
9+
onRetry: () => {},
10+
},
11+
};
12+
13+
export default meta;
14+
15+
type Story = StoryObj<typeof BackendErrorView>;
16+
17+
export const NetworkError: Story = {
18+
args: {
19+
error: 'TypeError: Failed to fetch — ECONNREFUSED',
20+
},
21+
};
22+
23+
export const AuthError: Story = {
24+
args: {
25+
error: '401 Unauthorized — session token expired',
26+
},
27+
};
28+
29+
export const TimeoutError: Story = {
30+
args: {
31+
error: 'AbortError: The operation was aborted (timed out)',
32+
},
33+
};
34+
35+
export const GenericError: Story = {
36+
args: {
37+
error: 'Internal server error: unexpected null pointer',
38+
},
39+
};

src/renderer/entities/contact/ui/BackendErrorView.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ export const BackendErrorView = ({ error, onRetry }: Props) => {
3434
const { t } = useI18n();
3535

3636
return (
37-
<div className="flex flex-col gap-y-3 py-4">
37+
<div className="py-4">
3838
<Alert title={t(getErrorMessageKey(error))} active variant="error">
3939
<CaptionText className="break-all text-text-tertiary">{error}</CaptionText>
40+
<Button variant="text" className="h-4.5 self-start p-0" onClick={onRetry}>
41+
{t('addressBook.sources.retry')}
42+
</Button>
4043
</Alert>
41-
<Button variant="text" className="h-4.5 self-center" onClick={onRetry}>
42-
{t('addressBook.sources.retry')}
43-
</Button>
4444
</div>
4545
);
4646
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { type Meta, type StoryObj } from '@storybook/react-vite';
2+
3+
import { BackendLoadingView } from './BackendLoadingView';
4+
5+
const meta: Meta<typeof BackendLoadingView> = {
6+
title: 'Address Book/BackendLoadingView',
7+
component: BackendLoadingView,
8+
};
9+
10+
export default meta;
11+
12+
type Story = StoryObj<typeof BackendLoadingView>;
13+
14+
export const Default: Story = {};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { type Meta, type StoryObj } from '@storybook/react-vite';
2+
import { MemoryRouter } from 'react-router-dom';
3+
4+
import { type BackendContact } from '@/shared/core';
5+
import { TEST_ACCOUNTS, TEST_ADDRESS } from '@/shared/lib/utils';
6+
7+
import { CachedWithErrorView } from './CachedWithErrorView';
8+
9+
const mockContacts: BackendContact[] = [
10+
{
11+
id: '1',
12+
name: 'Treasury Multisig',
13+
address: TEST_ADDRESS,
14+
accountId: TEST_ACCOUNTS[0],
15+
source: 'backend',
16+
entityNames: ['Nova Foundation'],
17+
chainId: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
18+
chainName: 'Polkadot',
19+
categoryName: 'Infrastructure',
20+
contactTypeName: 'Multisig',
21+
derivationPath: null,
22+
ownerAccountId: null,
23+
},
24+
{
25+
id: '2',
26+
name: 'Validator Node',
27+
address: TEST_ADDRESS,
28+
accountId: TEST_ACCOUNTS[1],
29+
source: 'backend',
30+
entityNames: ['Staking Ops'],
31+
chainId: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
32+
chainName: 'Kusama',
33+
categoryName: 'Validators',
34+
contactTypeName: null,
35+
derivationPath: null,
36+
ownerAccountId: null,
37+
},
38+
];
39+
40+
const meta: Meta<typeof CachedWithErrorView> = {
41+
title: 'Address Book/CachedWithErrorView',
42+
component: CachedWithErrorView,
43+
decorators: [
44+
(Story) => (
45+
<MemoryRouter>
46+
<Story />
47+
</MemoryRouter>
48+
),
49+
],
50+
args: {
51+
error: 'TypeError: Failed to fetch — ECONNREFUSED',
52+
items: mockContacts,
53+
onSendTo: () => {},
54+
onRetry: () => {},
55+
},
56+
};
57+
58+
export default meta;
59+
60+
type Story = StoryObj<typeof CachedWithErrorView>;
61+
62+
export const Default: Story = {};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { type Contact, isBackendContact, isLocalContact } from '@/shared/core';
2+
import { useI18n } from '@/shared/i18n';
3+
import { Alert, BodyText, Button } from '@/shared/ui';
4+
5+
import { BackendContactRow } from './BackendContactRow';
6+
import { ContactRow } from './ContactRow';
7+
8+
type Props = {
9+
error: string;
10+
items: Contact[];
11+
onSendTo: (contact: Contact) => void;
12+
onRetry: () => void;
13+
};
14+
15+
export const CachedWithErrorView = ({ error, items, onSendTo, onRetry }: Props) => {
16+
const { t } = useI18n();
17+
18+
return (
19+
<div className="flex flex-col gap-y-2">
20+
<Alert title={t('addressBook.sources.syncErrorCached')} active variant="warn">
21+
<Alert.Item withDot={false}>
22+
<div className="flex items-center gap-x-2">
23+
<BodyText className="break-all text-text-tertiary">{error}</BodyText>
24+
<Button variant="text" className="h-4.5 shrink-0" onClick={onRetry}>
25+
{t('addressBook.sources.retry')}
26+
</Button>
27+
</div>
28+
</Alert.Item>
29+
</Alert>
30+
<ul className="flex flex-col gap-y-2">
31+
{items.map((contact) =>
32+
isBackendContact(contact) ? (
33+
<BackendContactRow key={contact.id} contact={contact} onSendTo={onSendTo} />
34+
) : isLocalContact(contact) ? (
35+
<ContactRow key={contact.id} contact={contact} onSendTo={onSendTo} />
36+
) : null,
37+
)}
38+
</ul>
39+
</div>
40+
);
41+
};

0 commit comments

Comments
 (0)