@yargram/react 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.ts +21 -0
- package/.storybook/preview.ts +21 -0
- package/dist/components/LogWindow/LogEntryRow.d.ts +8 -0
- package/dist/components/LogWindow/LogEntryRow.d.ts.map +1 -0
- package/dist/components/LogWindow/LogEntryRow.js +14 -0
- package/dist/components/LogWindow/LogWindow.d.ts +41 -0
- package/dist/components/LogWindow/LogWindow.d.ts.map +1 -0
- package/dist/components/LogWindow/LogWindow.js +144 -0
- package/dist/components/LogWindow/LogWindow.stories.d.ts +29 -0
- package/dist/components/LogWindow/LogWindow.stories.d.ts.map +1 -0
- package/dist/components/LogWindow/LogWindow.stories.js +183 -0
- package/dist/components/LogWindow/LogWindow.test.d.ts +2 -0
- package/dist/components/LogWindow/LogWindow.test.d.ts.map +1 -0
- package/dist/components/LogWindow/LogWindow.test.js +61 -0
- package/dist/components/LogWindow/LogWindowEscapeDemo.d.ts +12 -0
- package/dist/components/LogWindow/LogWindowEscapeDemo.d.ts.map +1 -0
- package/dist/components/LogWindow/LogWindowEscapeDemo.js +56 -0
- package/dist/components/LogWindow/NetworkEntryRow.d.ts +8 -0
- package/dist/components/LogWindow/NetworkEntryRow.d.ts.map +1 -0
- package/dist/components/LogWindow/NetworkEntryRow.js +32 -0
- package/dist/components/LogWindow/index.d.ts +7 -0
- package/dist/components/LogWindow/index.d.ts.map +1 -0
- package/dist/components/LogWindow/index.js +4 -0
- package/dist/components/LogWindow/types.d.ts +36 -0
- package/dist/components/LogWindow/types.d.ts.map +1 -0
- package/dist/components/LogWindow/types.js +1 -0
- package/dist/components/LoginWindow/LoginForm.d.ts +11 -0
- package/dist/components/LoginWindow/LoginForm.d.ts.map +1 -0
- package/dist/components/LoginWindow/LoginForm.js +28 -0
- package/dist/components/LoginWindow/LoginForm.test.d.ts +2 -0
- package/dist/components/LoginWindow/LoginForm.test.d.ts.map +1 -0
- package/dist/components/LoginWindow/LoginForm.test.js +34 -0
- package/dist/components/LoginWindow/LoginWindow.d.ts +15 -0
- package/dist/components/LoginWindow/LoginWindow.d.ts.map +1 -0
- package/dist/components/LoginWindow/LoginWindow.js +27 -0
- package/dist/components/LoginWindow/index.d.ts +3 -0
- package/dist/components/LoginWindow/index.d.ts.map +1 -0
- package/dist/components/LoginWindow/index.js +1 -0
- package/dist/contexts/ApiContext.d.ts +35 -0
- package/dist/contexts/ApiContext.d.ts.map +1 -0
- package/dist/contexts/ApiContext.js +82 -0
- package/dist/contexts/ApiContext.test.d.ts +2 -0
- package/dist/contexts/ApiContext.test.d.ts.map +1 -0
- package/dist/contexts/ApiContext.test.js +45 -0
- package/dist/contexts/PrinterContext.d.ts +12 -0
- package/dist/contexts/PrinterContext.d.ts.map +1 -0
- package/dist/contexts/PrinterContext.js +17 -0
- package/dist/contexts/PrinterContext.test.d.ts +2 -0
- package/dist/contexts/PrinterContext.test.d.ts.map +1 -0
- package/dist/contexts/PrinterContext.test.js +19 -0
- package/dist/contexts/YahmanContext.d.ts +69 -0
- package/dist/contexts/YahmanContext.d.ts.map +1 -0
- package/dist/contexts/YahmanContext.js +414 -0
- package/dist/contexts/YahmanContext.stories.d.ts +16 -0
- package/dist/contexts/YahmanContext.stories.d.ts.map +1 -0
- package/dist/contexts/YahmanContext.stories.js +64 -0
- package/dist/contexts/YargramContext.d.ts +69 -0
- package/dist/contexts/YargramContext.d.ts.map +1 -0
- package/dist/contexts/YargramContext.js +414 -0
- package/dist/contexts/YargramContext.stories.d.ts +16 -0
- package/dist/contexts/YargramContext.stories.d.ts.map +1 -0
- package/dist/contexts/YargramContext.stories.js +64 -0
- package/dist/contexts/YargramContext.test.d.ts +2 -0
- package/dist/contexts/YargramContext.test.d.ts.map +1 -0
- package/dist/contexts/YargramContext.test.js +54 -0
- package/dist/hooks/useLogWindowShortcut.d.ts +24 -0
- package/dist/hooks/useLogWindowShortcut.d.ts.map +1 -0
- package/dist/hooks/useLogWindowShortcut.js +61 -0
- package/dist/hooks/useLogWindowShortcut.test.d.ts +2 -0
- package/dist/hooks/useLogWindowShortcut.test.d.ts.map +1 -0
- package/dist/hooks/useLogWindowShortcut.test.js +93 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/test/setup.d.ts +2 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/setup.js +1 -0
- package/package.json +49 -0
- package/src/components/LogWindow/LogEntryRow.tsx +38 -0
- package/src/components/LogWindow/LogWindow.css +614 -0
- package/src/components/LogWindow/LogWindow.stories.tsx +206 -0
- package/src/components/LogWindow/LogWindow.test.tsx +68 -0
- package/src/components/LogWindow/LogWindow.tsx +379 -0
- package/src/components/LogWindow/LogWindowEscapeDemo.tsx +100 -0
- package/src/components/LogWindow/NetworkEntryRow.tsx +102 -0
- package/src/components/LogWindow/index.ts +13 -0
- package/src/components/LogWindow/types.ts +40 -0
- package/src/components/LoginWindow/LoginForm.test.tsx +38 -0
- package/src/components/LoginWindow/LoginForm.tsx +78 -0
- package/src/components/LoginWindow/LoginWindow.css +198 -0
- package/src/components/LoginWindow/LoginWindow.tsx +90 -0
- package/src/components/LoginWindow/index.ts +2 -0
- package/src/contexts/ApiContext.test.tsx +68 -0
- package/src/contexts/ApiContext.tsx +155 -0
- package/src/contexts/PrinterContext.test.tsx +37 -0
- package/src/contexts/PrinterContext.tsx +35 -0
- package/src/contexts/YargramContext.stories.tsx +148 -0
- package/src/contexts/YargramContext.test.tsx +105 -0
- package/src/contexts/YargramContext.tsx +676 -0
- package/src/hooks/useLogWindowShortcut.test.ts +111 -0
- package/src/hooks/useLogWindowShortcut.ts +96 -0
- package/src/index.ts +14 -0
- package/src/test/setup.ts +1 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +18 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { ApiProvider, useApi } from './ApiContext';
|
|
6
|
+
|
|
7
|
+
function Consumer() {
|
|
8
|
+
const api = useApi();
|
|
9
|
+
const [result, setResult] = React.useState<string>('');
|
|
10
|
+
const callGet = () => {
|
|
11
|
+
api.get('/test').then((r) => setResult(`${r.status}`)).catch(() => setResult('error'));
|
|
12
|
+
};
|
|
13
|
+
return (
|
|
14
|
+
<div>
|
|
15
|
+
<span data-testid="provider">{api.provider}</span>
|
|
16
|
+
<button type="button" onClick={callGet}>
|
|
17
|
+
GET
|
|
18
|
+
</button>
|
|
19
|
+
<span data-testid="result">{result}</span>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('ApiProvider (REST)', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
27
|
+
ok: true,
|
|
28
|
+
status: 200,
|
|
29
|
+
statusText: 'OK',
|
|
30
|
+
clone: () => ({ text: () => Promise.resolve('{}') }),
|
|
31
|
+
}));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('provides REST api and useApi returns provider rest', () => {
|
|
35
|
+
render(
|
|
36
|
+
<ApiProvider provider="rest" baseUrl="https://api.example.com">
|
|
37
|
+
<Consumer />
|
|
38
|
+
</ApiProvider>
|
|
39
|
+
);
|
|
40
|
+
expect(screen.getByTestId('provider')).toHaveTextContent('rest');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('get() calls fetch with correct url', async () => {
|
|
44
|
+
const user = userEvent.setup();
|
|
45
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
46
|
+
ok: true,
|
|
47
|
+
status: 200,
|
|
48
|
+
statusText: 'OK',
|
|
49
|
+
clone: () => ({ text: () => Promise.resolve('[]') }),
|
|
50
|
+
});
|
|
51
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
52
|
+
render(
|
|
53
|
+
<ApiProvider provider="rest" baseUrl="https://api.example.com">
|
|
54
|
+
<Consumer />
|
|
55
|
+
</ApiProvider>
|
|
56
|
+
);
|
|
57
|
+
await user.click(screen.getByRole('button', { name: /get/i }));
|
|
58
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
59
|
+
'https://api.example.com/test',
|
|
60
|
+
expect.objectContaining({ method: 'GET' })
|
|
61
|
+
);
|
|
62
|
+
expect(screen.getByTestId('result')).toHaveTextContent('200');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('useApi throws when used outside provider', () => {
|
|
66
|
+
expect(() => render(<Consumer />)).toThrow('useApi must be used within ApiProvider');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ApolloClient,
|
|
4
|
+
ApolloProvider,
|
|
5
|
+
InMemoryCache,
|
|
6
|
+
type ApolloClient as ApolloClientType,
|
|
7
|
+
type NormalizedCacheObject,
|
|
8
|
+
type QueryOptions,
|
|
9
|
+
type MutationOptions,
|
|
10
|
+
type ApolloQueryResult,
|
|
11
|
+
type FetchResult,
|
|
12
|
+
type OperationVariables,
|
|
13
|
+
} from '@apollo/client';
|
|
14
|
+
|
|
15
|
+
export type ApiContextValue = RestApiContextValue | GraphqlApiContextValue;
|
|
16
|
+
|
|
17
|
+
/** REST: useApi().get / .post / .put / .delete で LogWindow Network に反映 */
|
|
18
|
+
export type RestApiContextValue = {
|
|
19
|
+
provider: 'rest';
|
|
20
|
+
get: (path: string, options?: RequestInit) => Promise<Response>;
|
|
21
|
+
post: (path: string, body?: BodyInit | Record<string, unknown>, options?: RequestInit) => Promise<Response>;
|
|
22
|
+
put: (path: string, body?: BodyInit | Record<string, unknown>, options?: RequestInit) => Promise<Response>;
|
|
23
|
+
delete: (path: string, options?: RequestInit) => Promise<Response>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** GraphQL: useApi().ransack (QUERY) / .handing (MUTATION) で LogWindow Network に反映 */
|
|
27
|
+
export type GraphqlApiContextValue = {
|
|
28
|
+
provider: 'graphql';
|
|
29
|
+
/** QUERY(戻り値は ApolloQueryResult、必要なら as でキャスト) */
|
|
30
|
+
ransack: <TData = unknown, TVariables = unknown>(
|
|
31
|
+
options: QueryOptions<TVariables, TData>
|
|
32
|
+
) => Promise<ApolloQueryResult<unknown>>;
|
|
33
|
+
/** MUTATION(戻り値は FetchResult、必要なら as でキャスト) */
|
|
34
|
+
handing: <TData = unknown, TVariables = unknown>(
|
|
35
|
+
options: MutationOptions<TData, TVariables>
|
|
36
|
+
) => Promise<FetchResult<unknown>>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const ApiContext = createContext<ApiContextValue | null>(null);
|
|
40
|
+
|
|
41
|
+
export type ApiProviderMode = 'rest' | 'graphql';
|
|
42
|
+
|
|
43
|
+
type ApiProviderProps =
|
|
44
|
+
| {
|
|
45
|
+
provider: 'rest';
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
baseUrl?: string;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
provider: 'graphql';
|
|
51
|
+
children: React.ReactNode;
|
|
52
|
+
uri?: string;
|
|
53
|
+
client?: ApolloClientType<NormalizedCacheObject>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function createApolloClient(uri: string) {
|
|
57
|
+
return new ApolloClient({
|
|
58
|
+
uri,
|
|
59
|
+
cache: new InMemoryCache(),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveBody(body?: BodyInit | Record<string, unknown>): BodyInit | undefined {
|
|
64
|
+
if (body == null) return undefined;
|
|
65
|
+
if (typeof body === 'string' || body instanceof ArrayBuffer || ArrayBuffer.isView(body) || body instanceof FormData || body instanceof URLSearchParams) {
|
|
66
|
+
return body;
|
|
67
|
+
}
|
|
68
|
+
return JSON.stringify(body);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ApiProvider(props: ApiProviderProps) {
|
|
72
|
+
const { children } = props;
|
|
73
|
+
const isGraphql = props.provider === 'graphql';
|
|
74
|
+
|
|
75
|
+
const graphqlUri = props.provider === 'graphql' ? props.uri : undefined;
|
|
76
|
+
const graphqlClientProp = props.provider === 'graphql' ? props.client : undefined;
|
|
77
|
+
const graphqlClient = useMemo(() => {
|
|
78
|
+
if (!isGraphql) return null;
|
|
79
|
+
if (graphqlClientProp) return graphqlClientProp;
|
|
80
|
+
const endpoint =
|
|
81
|
+
graphqlUri ?? (typeof process !== 'undefined' ? process.env?.GRAPHQL_URI : '');
|
|
82
|
+
if (!endpoint) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
'ApiProvider(provider="graphql") requires either "uri" prop or GRAPHQL_URI environment variable'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return createApolloClient(endpoint);
|
|
88
|
+
}, [isGraphql, graphqlUri, graphqlClientProp]);
|
|
89
|
+
|
|
90
|
+
const baseUrl = props.provider === 'rest' ? props.baseUrl : undefined;
|
|
91
|
+
const endpoint =
|
|
92
|
+
baseUrl ?? (typeof process !== 'undefined' ? process.env?.ENDPOINT_URL : '') ?? '';
|
|
93
|
+
|
|
94
|
+
const restValue = useMemo<RestApiContextValue | null>(() => {
|
|
95
|
+
if (isGraphql) return null;
|
|
96
|
+
const url = (path: string) => `${endpoint}${path}`;
|
|
97
|
+
return {
|
|
98
|
+
provider: 'rest',
|
|
99
|
+
get: (path, options) => fetch(url(path), { ...options, method: 'GET' }),
|
|
100
|
+
post: (path, body, options) =>
|
|
101
|
+
fetch(url(path), {
|
|
102
|
+
...options,
|
|
103
|
+
method: 'POST',
|
|
104
|
+
body: resolveBody(body),
|
|
105
|
+
headers:
|
|
106
|
+
body != null && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof URLSearchParams)
|
|
107
|
+
? { 'Content-Type': 'application/json', ...options?.headers }
|
|
108
|
+
: options?.headers,
|
|
109
|
+
}),
|
|
110
|
+
put: (path, body, options) =>
|
|
111
|
+
fetch(url(path), {
|
|
112
|
+
...options,
|
|
113
|
+
method: 'PUT',
|
|
114
|
+
body: resolveBody(body),
|
|
115
|
+
headers:
|
|
116
|
+
body != null && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof URLSearchParams)
|
|
117
|
+
? { 'Content-Type': 'application/json', ...options?.headers }
|
|
118
|
+
: options?.headers,
|
|
119
|
+
}),
|
|
120
|
+
delete: (path, options) => fetch(url(path), { ...options, method: 'DELETE' }),
|
|
121
|
+
};
|
|
122
|
+
}, [isGraphql, endpoint]);
|
|
123
|
+
|
|
124
|
+
if (isGraphql && graphqlClient) {
|
|
125
|
+
const graphqlValue: GraphqlApiContextValue = useMemo(
|
|
126
|
+
() => ({
|
|
127
|
+
provider: 'graphql',
|
|
128
|
+
ransack: (options) =>
|
|
129
|
+
graphqlClient.query(options as QueryOptions<OperationVariables, unknown>),
|
|
130
|
+
handing: (options) =>
|
|
131
|
+
graphqlClient.mutate(options as MutationOptions<unknown, OperationVariables>),
|
|
132
|
+
}),
|
|
133
|
+
[graphqlClient]
|
|
134
|
+
);
|
|
135
|
+
return (
|
|
136
|
+
<ApolloProvider client={graphqlClient}>
|
|
137
|
+
<ApiContext.Provider value={graphqlValue}>{children}</ApiContext.Provider>
|
|
138
|
+
</ApolloProvider>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (restValue) {
|
|
143
|
+
return <ApiContext.Provider value={restValue}>{children}</ApiContext.Provider>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return <>{children}</>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function useApi(): ApiContextValue {
|
|
150
|
+
const ctx = useContext(ApiContext);
|
|
151
|
+
if (!ctx) {
|
|
152
|
+
throw new Error('useApi must be used within ApiProvider or YargramProvider');
|
|
153
|
+
}
|
|
154
|
+
return ctx;
|
|
155
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { PrinterProvider, usePrinter } from './PrinterContext';
|
|
4
|
+
|
|
5
|
+
function Consumer() {
|
|
6
|
+
const printer = usePrinter();
|
|
7
|
+
return (
|
|
8
|
+
<div>
|
|
9
|
+
<button type="button" onClick={() => printer.info('info message')}>
|
|
10
|
+
Info
|
|
11
|
+
</button>
|
|
12
|
+
<button type="button" onClick={() => printer.warn('warn message')}>
|
|
13
|
+
Warn
|
|
14
|
+
</button>
|
|
15
|
+
<button type="button" onClick={() => printer.error('error message')}>
|
|
16
|
+
Error
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('PrinterProvider', () => {
|
|
23
|
+
it('renders children', () => {
|
|
24
|
+
render(
|
|
25
|
+
<PrinterProvider>
|
|
26
|
+
<Consumer />
|
|
27
|
+
</PrinterProvider>
|
|
28
|
+
);
|
|
29
|
+
expect(screen.getByRole('button', { name: /info/i })).toBeInTheDocument();
|
|
30
|
+
expect(screen.getByRole('button', { name: /warn/i })).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByRole('button', { name: /error/i })).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('usePrinter throws when used outside provider', () => {
|
|
35
|
+
expect(() => render(<Consumer />)).toThrow('usePrinter must be used within PrinterProvider');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import { createPrinter, type Env } from '@yargram/core';
|
|
3
|
+
|
|
4
|
+
export type Printer = ReturnType<typeof createPrinter>;
|
|
5
|
+
|
|
6
|
+
type PrinterContextValue = {
|
|
7
|
+
printer: Printer;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const PrinterContext = createContext<PrinterContextValue | null>(null);
|
|
11
|
+
|
|
12
|
+
export type PrinterProviderProps = {
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
env?: Env;
|
|
15
|
+
/** 指定時は createPrinter(env) の代わりにこの printer を利用(YargramProvider で addLogEntry 連携用) */
|
|
16
|
+
printer?: Printer;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function PrinterProvider({ children, env = 'local', printer: printerProp }: PrinterProviderProps) {
|
|
20
|
+
const defaultPrinter = useMemo(() => createPrinter(env), [env]);
|
|
21
|
+
const printer = printerProp ?? defaultPrinter;
|
|
22
|
+
const value = useMemo(() => ({ printer }), [printer]);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<PrinterContext.Provider value={value}>{children}</PrinterContext.Provider>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function usePrinter(): Printer {
|
|
30
|
+
const ctx = useContext(PrinterContext);
|
|
31
|
+
if (!ctx) {
|
|
32
|
+
throw new Error('usePrinter must be used within PrinterProvider');
|
|
33
|
+
}
|
|
34
|
+
return ctx.printer;
|
|
35
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { gql } from '@apollo/client';
|
|
3
|
+
import { YargramProvider, useYargram } from './YargramContext';
|
|
4
|
+
import { usePrinter } from './PrinterContext';
|
|
5
|
+
import { useApi } from './ApiContext';
|
|
6
|
+
|
|
7
|
+
function DemoContent() {
|
|
8
|
+
const { openLogWindow } = useYargram();
|
|
9
|
+
const printer = usePrinter();
|
|
10
|
+
const api = useApi();
|
|
11
|
+
|
|
12
|
+
const addInfo = () => printer.info('Info from usePrinter');
|
|
13
|
+
const addWarn = () => printer.warn('Warn from usePrinter');
|
|
14
|
+
const addError = () => printer.error('Error from usePrinter');
|
|
15
|
+
|
|
16
|
+
if (api.provider === 'rest') {
|
|
17
|
+
return (
|
|
18
|
+
<div style={{ padding: 24, fontFamily: 'sans-serif' }}>
|
|
19
|
+
<h3>YargramProvider デモ (REST)</h3>
|
|
20
|
+
<p>Escape キーを 5 回押すとログウィンドウが開きます。</p>
|
|
21
|
+
<p style={{ fontSize: 12, color: '#666', marginTop: 8 }}>
|
|
22
|
+
usePrinter().info / warn / error → Log タブ。useApi().get / post / put / delete → Network タブに反映。
|
|
23
|
+
</p>
|
|
24
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 16 }}>
|
|
25
|
+
<button type="button" onClick={addInfo}>Log: Info</button>
|
|
26
|
+
<button type="button" onClick={addWarn}>Log: Warn</button>
|
|
27
|
+
<button type="button" onClick={addError}>Log: Error</button>
|
|
28
|
+
<button type="button" onClick={() => api.get('/posts').then((res) => printer.info(`Response: ${res.status} ${res.statusText}`)).catch((err) => printer.error(`Error: ${err.message}`))}>
|
|
29
|
+
Network: GET /posts
|
|
30
|
+
</button>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={() =>
|
|
34
|
+
api
|
|
35
|
+
.post('/posts', { title: 'Storybook post', body: 'Body from demo', userId: 1 })
|
|
36
|
+
.then((res) => printer.info(`Response: ${res.status} ${res.statusText}`))
|
|
37
|
+
.catch((err) => printer.error(`Error: ${err.message}`))
|
|
38
|
+
}
|
|
39
|
+
>
|
|
40
|
+
Network: POST /posts
|
|
41
|
+
</button>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={() =>
|
|
45
|
+
api
|
|
46
|
+
.put('/posts/1', { id: 1, title: 'Updated title', body: 'Updated body', userId: 1 })
|
|
47
|
+
.then((res) => printer.info(`Response: ${res.status} ${res.statusText}`))
|
|
48
|
+
.catch((err) => printer.error(`Error: ${err.message}`))
|
|
49
|
+
}
|
|
50
|
+
>
|
|
51
|
+
Network: PUT /posts/1
|
|
52
|
+
</button>
|
|
53
|
+
<button type="button" onClick={() => api.delete('/posts/1').then((res) => printer.info(`Response: ${res.status} ${res.statusText}`)).catch(() => {})}>
|
|
54
|
+
Network: DELETE /posts/1
|
|
55
|
+
</button>
|
|
56
|
+
<button type="button" onClick={openLogWindow}>ログウィンドウを開く</button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const runQuery = () => {
|
|
63
|
+
api
|
|
64
|
+
.ransack({
|
|
65
|
+
query: gql`
|
|
66
|
+
query GetUser { user(id: "1") { id name } }
|
|
67
|
+
`,
|
|
68
|
+
})
|
|
69
|
+
.catch((err) => printer.error(`Error: ${err.message}`));
|
|
70
|
+
};
|
|
71
|
+
const runMutation = () => {
|
|
72
|
+
api
|
|
73
|
+
.handing({
|
|
74
|
+
mutation: gql`
|
|
75
|
+
mutation UpdateUser { updateUser(id: "1", name: "x") { id name } }
|
|
76
|
+
`,
|
|
77
|
+
})
|
|
78
|
+
.catch((err) => printer.error(`Error: ${err.message}`));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div style={{ padding: 24, fontFamily: 'sans-serif' }}>
|
|
83
|
+
<h3>YargramProvider デモ (GraphQL)</h3>
|
|
84
|
+
<p>useApi().ransack (QUERY) / .handing (MUTATION) で Network タブに反映。</p>
|
|
85
|
+
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', marginTop: 16 }}>
|
|
86
|
+
<button type="button" onClick={addInfo}>Log: Info</button>
|
|
87
|
+
<button type="button" onClick={runQuery}>Network: ransack (QUERY)</button>
|
|
88
|
+
<button type="button" onClick={runMutation}>Network: handing (MUTATION)</button>
|
|
89
|
+
<button type="button" onClick={openLogWindow}>ログウィンドウを開く</button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const meta: Meta<typeof YargramProvider> = {
|
|
96
|
+
title: 'Contexts/YargramProvider',
|
|
97
|
+
component: YargramProvider,
|
|
98
|
+
parameters: { layout: 'centered' },
|
|
99
|
+
tags: ['autodocs'],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export default meta;
|
|
103
|
+
|
|
104
|
+
type Story = StoryObj<typeof YargramProvider>;
|
|
105
|
+
|
|
106
|
+
/** Api (REST) + Printer + LogWindow(Escape 5回で表示)。REST は JSONPlaceholder /posts を使用 */
|
|
107
|
+
export const Default: Story = {
|
|
108
|
+
render: () => (
|
|
109
|
+
<YargramProvider
|
|
110
|
+
api={{ provider: 'rest', baseUrl: 'https://jsonplaceholder.typicode.com' }}
|
|
111
|
+
printer={{ env: 'local' }}
|
|
112
|
+
logWindow={{}}
|
|
113
|
+
>
|
|
114
|
+
<DemoContent />
|
|
115
|
+
</YargramProvider>
|
|
116
|
+
),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 本番時のみ認証(auth: true)。
|
|
121
|
+
* storybookSimulateProduction: true で Storybook 内だけ本番扱いし、ログイン画面を表示。
|
|
122
|
+
* 本番ビルド時は NODE_ENV=production で同様にログイン要求。
|
|
123
|
+
*/
|
|
124
|
+
export const WithAuthProductionOnly: Story = {
|
|
125
|
+
render: () => (
|
|
126
|
+
<YargramProvider
|
|
127
|
+
api={{ provider: 'rest', baseUrl: 'https://jsonplaceholder.typicode.com' }}
|
|
128
|
+
printer={{ env: 'local' }}
|
|
129
|
+
logWindow={{}}
|
|
130
|
+
auth={{ storybookSimulateProduction: true }}
|
|
131
|
+
>
|
|
132
|
+
<DemoContent />
|
|
133
|
+
</YargramProvider>
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** GraphQL: useApi().ransack (QUERY) / .handing (MUTATION) → Network */
|
|
138
|
+
export const GraphQL: Story = {
|
|
139
|
+
render: () => (
|
|
140
|
+
<YargramProvider
|
|
141
|
+
api={{ provider: 'graphql', uri: 'https://api.example.com/graphql' }}
|
|
142
|
+
printer={{ env: 'local' }}
|
|
143
|
+
logWindow={{}}
|
|
144
|
+
>
|
|
145
|
+
<DemoContent />
|
|
146
|
+
</YargramProvider>
|
|
147
|
+
),
|
|
148
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { YargramProvider, useYargram } from './YargramContext';
|
|
5
|
+
import { usePrinter } from './PrinterContext';
|
|
6
|
+
import { useApi } from './ApiContext';
|
|
7
|
+
|
|
8
|
+
vi.mock('react-dom', async () => {
|
|
9
|
+
const actual = await vi.importActual<typeof import('react-dom')>('react-dom');
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
createPortal: (children: React.ReactNode) => children,
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function Consumer() {
|
|
17
|
+
const yargram = useYargram();
|
|
18
|
+
const printer = usePrinter();
|
|
19
|
+
const api = useApi();
|
|
20
|
+
return (
|
|
21
|
+
<div>
|
|
22
|
+
<span data-testid="is-open">{String(yargram.isLogWindowOpen)}</span>
|
|
23
|
+
<span data-testid="log-entries-count">{yargram.logEntries.length}</span>
|
|
24
|
+
<button type="button" onClick={yargram.openLogWindow}>
|
|
25
|
+
Open
|
|
26
|
+
</button>
|
|
27
|
+
<button type="button" onClick={yargram.closeLogWindow} data-testid="close-log-window-btn">
|
|
28
|
+
Close
|
|
29
|
+
</button>
|
|
30
|
+
<button
|
|
31
|
+
type="button"
|
|
32
|
+
onClick={() => {
|
|
33
|
+
printer.info('hello');
|
|
34
|
+
yargram.addLogEntry({ level: 'info', message: 'manual', source: 'test' });
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
Add log
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('YargramProvider', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, status: 200, statusText: 'OK', clone: () => ({ text: () => Promise.resolve('{}') }) }));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('renders children and provides useYargram', () => {
|
|
49
|
+
render(
|
|
50
|
+
<YargramProvider
|
|
51
|
+
api={{ provider: 'rest', baseUrl: 'https://api.example.com' }}
|
|
52
|
+
logWindow={{}}
|
|
53
|
+
>
|
|
54
|
+
<Consumer />
|
|
55
|
+
</YargramProvider>
|
|
56
|
+
);
|
|
57
|
+
expect(screen.getByTestId('is-open')).toHaveTextContent('false');
|
|
58
|
+
expect(screen.getByTestId('log-entries-count')).toHaveTextContent('0');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('openLogWindow sets isLogWindowOpen to true', async () => {
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
render(
|
|
64
|
+
<YargramProvider
|
|
65
|
+
api={{ provider: 'rest', baseUrl: 'https://api.example.com' }}
|
|
66
|
+
logWindow={{}}
|
|
67
|
+
>
|
|
68
|
+
<Consumer />
|
|
69
|
+
</YargramProvider>
|
|
70
|
+
);
|
|
71
|
+
await user.click(screen.getByRole('button', { name: /open/i }));
|
|
72
|
+
expect(screen.getByTestId('is-open')).toHaveTextContent('true');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('addLogEntry adds to logEntries', async () => {
|
|
76
|
+
const user = userEvent.setup();
|
|
77
|
+
render(
|
|
78
|
+
<YargramProvider
|
|
79
|
+
api={{ provider: 'rest', baseUrl: 'https://api.example.com' }}
|
|
80
|
+
logWindow={{}}
|
|
81
|
+
>
|
|
82
|
+
<Consumer />
|
|
83
|
+
</YargramProvider>
|
|
84
|
+
);
|
|
85
|
+
expect(screen.getByTestId('log-entries-count')).toHaveTextContent('0');
|
|
86
|
+
await user.click(screen.getByRole('button', { name: /add log/i }));
|
|
87
|
+
expect(screen.getByTestId('log-entries-count')).toHaveTextContent('2');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('closeLogWindow sets isLogWindowOpen to false', async () => {
|
|
91
|
+
const user = userEvent.setup();
|
|
92
|
+
render(
|
|
93
|
+
<YargramProvider
|
|
94
|
+
api={{ provider: 'rest', baseUrl: 'https://api.example.com' }}
|
|
95
|
+
logWindow={{}}
|
|
96
|
+
>
|
|
97
|
+
<Consumer />
|
|
98
|
+
</YargramProvider>
|
|
99
|
+
);
|
|
100
|
+
await user.click(screen.getByRole('button', { name: /open/i }));
|
|
101
|
+
expect(screen.getByTestId('is-open')).toHaveTextContent('true');
|
|
102
|
+
await user.click(screen.getByTestId('close-log-window-btn'));
|
|
103
|
+
expect(screen.getByTestId('is-open')).toHaveTextContent('false');
|
|
104
|
+
});
|
|
105
|
+
});
|