@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.
Files changed (105) hide show
  1. package/.storybook/main.ts +21 -0
  2. package/.storybook/preview.ts +21 -0
  3. package/dist/components/LogWindow/LogEntryRow.d.ts +8 -0
  4. package/dist/components/LogWindow/LogEntryRow.d.ts.map +1 -0
  5. package/dist/components/LogWindow/LogEntryRow.js +14 -0
  6. package/dist/components/LogWindow/LogWindow.d.ts +41 -0
  7. package/dist/components/LogWindow/LogWindow.d.ts.map +1 -0
  8. package/dist/components/LogWindow/LogWindow.js +144 -0
  9. package/dist/components/LogWindow/LogWindow.stories.d.ts +29 -0
  10. package/dist/components/LogWindow/LogWindow.stories.d.ts.map +1 -0
  11. package/dist/components/LogWindow/LogWindow.stories.js +183 -0
  12. package/dist/components/LogWindow/LogWindow.test.d.ts +2 -0
  13. package/dist/components/LogWindow/LogWindow.test.d.ts.map +1 -0
  14. package/dist/components/LogWindow/LogWindow.test.js +61 -0
  15. package/dist/components/LogWindow/LogWindowEscapeDemo.d.ts +12 -0
  16. package/dist/components/LogWindow/LogWindowEscapeDemo.d.ts.map +1 -0
  17. package/dist/components/LogWindow/LogWindowEscapeDemo.js +56 -0
  18. package/dist/components/LogWindow/NetworkEntryRow.d.ts +8 -0
  19. package/dist/components/LogWindow/NetworkEntryRow.d.ts.map +1 -0
  20. package/dist/components/LogWindow/NetworkEntryRow.js +32 -0
  21. package/dist/components/LogWindow/index.d.ts +7 -0
  22. package/dist/components/LogWindow/index.d.ts.map +1 -0
  23. package/dist/components/LogWindow/index.js +4 -0
  24. package/dist/components/LogWindow/types.d.ts +36 -0
  25. package/dist/components/LogWindow/types.d.ts.map +1 -0
  26. package/dist/components/LogWindow/types.js +1 -0
  27. package/dist/components/LoginWindow/LoginForm.d.ts +11 -0
  28. package/dist/components/LoginWindow/LoginForm.d.ts.map +1 -0
  29. package/dist/components/LoginWindow/LoginForm.js +28 -0
  30. package/dist/components/LoginWindow/LoginForm.test.d.ts +2 -0
  31. package/dist/components/LoginWindow/LoginForm.test.d.ts.map +1 -0
  32. package/dist/components/LoginWindow/LoginForm.test.js +34 -0
  33. package/dist/components/LoginWindow/LoginWindow.d.ts +15 -0
  34. package/dist/components/LoginWindow/LoginWindow.d.ts.map +1 -0
  35. package/dist/components/LoginWindow/LoginWindow.js +27 -0
  36. package/dist/components/LoginWindow/index.d.ts +3 -0
  37. package/dist/components/LoginWindow/index.d.ts.map +1 -0
  38. package/dist/components/LoginWindow/index.js +1 -0
  39. package/dist/contexts/ApiContext.d.ts +35 -0
  40. package/dist/contexts/ApiContext.d.ts.map +1 -0
  41. package/dist/contexts/ApiContext.js +82 -0
  42. package/dist/contexts/ApiContext.test.d.ts +2 -0
  43. package/dist/contexts/ApiContext.test.d.ts.map +1 -0
  44. package/dist/contexts/ApiContext.test.js +45 -0
  45. package/dist/contexts/PrinterContext.d.ts +12 -0
  46. package/dist/contexts/PrinterContext.d.ts.map +1 -0
  47. package/dist/contexts/PrinterContext.js +17 -0
  48. package/dist/contexts/PrinterContext.test.d.ts +2 -0
  49. package/dist/contexts/PrinterContext.test.d.ts.map +1 -0
  50. package/dist/contexts/PrinterContext.test.js +19 -0
  51. package/dist/contexts/YahmanContext.d.ts +69 -0
  52. package/dist/contexts/YahmanContext.d.ts.map +1 -0
  53. package/dist/contexts/YahmanContext.js +414 -0
  54. package/dist/contexts/YahmanContext.stories.d.ts +16 -0
  55. package/dist/contexts/YahmanContext.stories.d.ts.map +1 -0
  56. package/dist/contexts/YahmanContext.stories.js +64 -0
  57. package/dist/contexts/YargramContext.d.ts +69 -0
  58. package/dist/contexts/YargramContext.d.ts.map +1 -0
  59. package/dist/contexts/YargramContext.js +414 -0
  60. package/dist/contexts/YargramContext.stories.d.ts +16 -0
  61. package/dist/contexts/YargramContext.stories.d.ts.map +1 -0
  62. package/dist/contexts/YargramContext.stories.js +64 -0
  63. package/dist/contexts/YargramContext.test.d.ts +2 -0
  64. package/dist/contexts/YargramContext.test.d.ts.map +1 -0
  65. package/dist/contexts/YargramContext.test.js +54 -0
  66. package/dist/hooks/useLogWindowShortcut.d.ts +24 -0
  67. package/dist/hooks/useLogWindowShortcut.d.ts.map +1 -0
  68. package/dist/hooks/useLogWindowShortcut.js +61 -0
  69. package/dist/hooks/useLogWindowShortcut.test.d.ts +2 -0
  70. package/dist/hooks/useLogWindowShortcut.test.d.ts.map +1 -0
  71. package/dist/hooks/useLogWindowShortcut.test.js +93 -0
  72. package/dist/index.d.ts +6 -0
  73. package/dist/index.d.ts.map +1 -0
  74. package/dist/index.js +7 -0
  75. package/dist/test/setup.d.ts +2 -0
  76. package/dist/test/setup.d.ts.map +1 -0
  77. package/dist/test/setup.js +1 -0
  78. package/package.json +49 -0
  79. package/src/components/LogWindow/LogEntryRow.tsx +38 -0
  80. package/src/components/LogWindow/LogWindow.css +614 -0
  81. package/src/components/LogWindow/LogWindow.stories.tsx +206 -0
  82. package/src/components/LogWindow/LogWindow.test.tsx +68 -0
  83. package/src/components/LogWindow/LogWindow.tsx +379 -0
  84. package/src/components/LogWindow/LogWindowEscapeDemo.tsx +100 -0
  85. package/src/components/LogWindow/NetworkEntryRow.tsx +102 -0
  86. package/src/components/LogWindow/index.ts +13 -0
  87. package/src/components/LogWindow/types.ts +40 -0
  88. package/src/components/LoginWindow/LoginForm.test.tsx +38 -0
  89. package/src/components/LoginWindow/LoginForm.tsx +78 -0
  90. package/src/components/LoginWindow/LoginWindow.css +198 -0
  91. package/src/components/LoginWindow/LoginWindow.tsx +90 -0
  92. package/src/components/LoginWindow/index.ts +2 -0
  93. package/src/contexts/ApiContext.test.tsx +68 -0
  94. package/src/contexts/ApiContext.tsx +155 -0
  95. package/src/contexts/PrinterContext.test.tsx +37 -0
  96. package/src/contexts/PrinterContext.tsx +35 -0
  97. package/src/contexts/YargramContext.stories.tsx +148 -0
  98. package/src/contexts/YargramContext.test.tsx +105 -0
  99. package/src/contexts/YargramContext.tsx +676 -0
  100. package/src/hooks/useLogWindowShortcut.test.ts +111 -0
  101. package/src/hooks/useLogWindowShortcut.ts +96 -0
  102. package/src/index.ts +14 -0
  103. package/src/test/setup.ts +1 -0
  104. package/tsconfig.json +16 -0
  105. 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
+ });