@proteinjs/db-ui 1.8.4 → 1.9.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 (41) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/src/pages/HashGeneratorPage.d.ts.map +1 -1
  5. package/dist/src/pages/HashGeneratorPage.js +0 -1
  6. package/dist/src/pages/HashGeneratorPage.js.map +1 -1
  7. package/dist/src/pages/RecordFormPage.d.ts.map +1 -1
  8. package/dist/src/pages/RecordFormPage.js +13 -6
  9. package/dist/src/pages/RecordFormPage.js.map +1 -1
  10. package/dist/src/pages/RecordTablePage.d.ts.map +1 -1
  11. package/dist/src/pages/RecordTablePage.js +9 -6
  12. package/dist/src/pages/RecordTablePage.js.map +1 -1
  13. package/dist/src/pages/TablesPage.d.ts.map +1 -1
  14. package/dist/src/pages/TablesPage.js +0 -1
  15. package/dist/src/pages/TablesPage.js.map +1 -1
  16. package/dist/src/pages/UuidGeneratorPage.d.ts.map +1 -1
  17. package/dist/src/pages/UuidGeneratorPage.js +0 -1
  18. package/dist/src/pages/UuidGeneratorPage.js.map +1 -1
  19. package/dist/src/table/QueryCursorLoader.d.ts +10 -14
  20. package/dist/src/table/QueryCursorLoader.d.ts.map +1 -1
  21. package/dist/src/table/QueryCursorLoader.js +20 -41
  22. package/dist/src/table/QueryCursorLoader.js.map +1 -1
  23. package/dist/test/QueryCursorLoader.test.d.ts +1 -1
  24. package/dist/test/QueryCursorLoader.test.d.ts.map +1 -1
  25. package/dist/test/QueryCursorLoader.test.js +122 -95
  26. package/dist/test/QueryCursorLoader.test.js.map +1 -1
  27. package/dist/test/recordPageErrorStates.test.d.ts +2 -0
  28. package/dist/test/recordPageErrorStates.test.d.ts.map +1 -0
  29. package/dist/test/recordPageErrorStates.test.js +37 -0
  30. package/dist/test/recordPageErrorStates.test.js.map +1 -0
  31. package/generated/index.ts +1 -1
  32. package/jest.config.js +8 -0
  33. package/package.json +4 -3
  34. package/src/pages/HashGeneratorPage.tsx +0 -1
  35. package/src/pages/RecordFormPage.tsx +13 -7
  36. package/src/pages/RecordTablePage.tsx +14 -11
  37. package/src/pages/TablesPage.tsx +0 -1
  38. package/src/pages/UuidGeneratorPage.tsx +0 -1
  39. package/src/table/QueryCursorLoader.ts +23 -43
  40. package/test/QueryCursorLoader.test.ts +87 -94
  41. package/test/recordPageErrorStates.test.tsx +35 -0
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { Page, PageComponentProps } from '@proteinjs/ui';
3
3
  import { tableByName, Table } from '@proteinjs/db';
4
4
  import { RecordTable } from '../table/RecordTable';
5
- import { Box, Paper, SxProps, Theme } from '@mui/material';
5
+ import { Box, Paper, SxProps, Theme, Typography } from '@mui/material';
6
6
 
7
7
  export const recordTablePage: Page = {
8
8
  name: 'Record Table',
@@ -10,19 +10,14 @@ export const recordTablePage: Page = {
10
10
  auth: {
11
11
  allUsers: true,
12
12
  },
13
+ // No height override: the app's page container owns viewport height (dvh on mobile —
14
+ // a 100vh pin here clipped the bottom behind mobile browser chrome).
13
15
  pageContainerSxProps: (theme: Theme): SxProps => {
14
16
  return {
15
- height: '100vh',
16
17
  backgroundColor: theme.palette.background.default,
17
18
  };
18
19
  },
19
- component: ({ ...props }) => (
20
- <Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center', padding: 4 }}>
21
- <Paper sx={{ maxHeight: '80vh' }}>
22
- <DynamicRecordTable {...props} />
23
- </Paper>
24
- </Box>
25
- ),
20
+ component: ({ ...props }) => <DynamicRecordTable {...props} />,
26
21
  };
27
22
 
28
23
  export const recordTableLink = (table: Table<any>) => {
@@ -48,11 +43,19 @@ const DynamicRecordTable = ({ urlParams }: PageComponentProps) => {
48
43
  errorMessage = `Table not provided via the 'name' url param`;
49
44
  }
50
45
 
46
+ // The error state renders as a plain message — wrapping it in the table's stretched
47
+ // card produced a full-height empty Paper with the text clipped at its edge.
51
48
  if (!table) {
52
- return <div>{errorMessage}</div>;
49
+ return <Typography sx={{ p: 3, color: 'text.secondary' }}>{errorMessage}</Typography>;
53
50
  }
54
51
 
55
- return <RecordTable table={table} />;
52
+ return (
53
+ <Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center', padding: 4 }}>
54
+ <Paper sx={{ maxHeight: '80vh' }}>
55
+ <RecordTable table={table} />
56
+ </Paper>
57
+ </Box>
58
+ );
56
59
  }
57
60
 
58
61
  return <Table />;
@@ -16,7 +16,6 @@ export const tablesPage: Page = {
16
16
  auth: { permission: 'dev' },
17
17
  pageContainerSxProps: (theme: Theme): SxProps => {
18
18
  return {
19
- height: '100vh',
20
19
  backgroundColor: theme.palette.background.default,
21
20
  };
22
21
  },
@@ -10,7 +10,6 @@ export const uuidGeneratorPage: Page = {
10
10
  auth: { permission: 'dev' },
11
11
  pageContainerSxProps: (theme: Theme): SxProps => {
12
12
  return {
13
- height: '100vh',
14
13
  backgroundColor: theme.palette.background.default,
15
14
  };
16
15
  },
@@ -1,70 +1,50 @@
1
- import { QueryBuilder, QueryOptions, Record, SortCriteria, Table, getDb } from '@proteinjs/db';
1
+ import { CursorWindowPager, QueryBuilder, QueryOptions, Record, SortCriteria, Table } from '@proteinjs/db';
2
2
  import { CursorLoader, CursorValue, CursorWindow, ReactQueryKeys } from '@proteinjs/ui';
3
3
 
4
4
  /**
5
- * `CursorLoader` over the same QueryBuilder machinery as `QueryTableLoader`: each window
6
- * applies `cursorField < cursor` (`>` for an ascending sort) plus `paginate(0, windowSize)`
7
- * on a fresh build of the base query, so rows created or removed while paging can never
8
- * shift the window frame. The cursor field and direction derive from the PRIMARY sort
9
- * criterion.
5
+ * `CursorLoader` over the db-owned cursor-window machinery (`CursorWindowPager`): each window
6
+ * carries a lexicographic continuation past the anchor row — the primary sort axes plus a
7
+ * unique `id` tiebreak — and `paginate(0, windowSize)` on a fresh build of the base query, so
8
+ * rows created or removed while paging can never shift the window frame, and ties on the sort
9
+ * axis can never skip or duplicate boundary rows. Cursors are the pager's anchors encoded as
10
+ * strings (house `Serializer`, so dates/moments survive react-query page params).
10
11
  *
11
12
  * `dataKey` is the table name — table-level invalidation (`useTableMutation`) reaches every
12
13
  * cursor query over it; `dataQueryKey` is the serialized query + sort.
13
14
  */
14
15
  export class QueryCursorLoader<T extends Record> implements CursorLoader<T> {
15
16
  reactQueryKeys: ReactQueryKeys;
16
- private sort: SortCriteria<T>[];
17
+ private pager: CursorWindowPager<T>;
17
18
 
18
19
  /**
19
20
  * @param table the table to load windows from
20
21
  * @param createQuery factory producing a FRESH conditions-only QueryBuilder per call —
21
- * builders are mutable, so every window builds its own to hang the cursor condition,
22
+ * builders are mutable, so every window builds its own to hang the cursor conditions,
22
23
  * sort, and pagination on
23
24
  * @param sort the sort criteria to apply to every window; the primary criterion is the
24
- * cursor axis
25
+ * cursor axis (the pager appends the `id` tiebreak)
25
26
  * @param queryOptions query options passed through to every window query
26
27
  */
27
28
  constructor(
28
- private table: Table<T>,
29
- private createQuery: () => QueryBuilder<T>,
29
+ table: Table<T>,
30
+ createQuery: () => QueryBuilder<T>,
30
31
  sort?: SortCriteria<T>[],
31
- private queryOptions?: QueryOptions<T>
32
+ queryOptions?: QueryOptions<T>
32
33
  ) {
33
- this.sort = sort ? sort : [{ field: 'created' as keyof T, desc: true }];
34
+ const effectiveSort = sort ? sort : [{ field: 'created' as keyof T, desc: true }];
35
+ this.pager = new CursorWindowPager<T>(table, createQuery, effectiveSort, { queryOptions });
34
36
  this.reactQueryKeys = {
35
- dataKey: this.table.name,
36
- dataQueryKey: JSON.stringify({ query: this.createQuery(), sort: this.sort }),
37
+ dataKey: table.name,
38
+ dataQueryKey: JSON.stringify({ query: createQuery(), sort: effectiveSort }),
37
39
  };
38
40
  }
39
41
 
40
42
  async loadWindow(cursor: CursorValue | null, windowSize: number): Promise<CursorWindow<T>> {
41
- const qb = this.buildWindowQuery(cursor, windowSize);
42
- const rows = await getDb().query(this.table, qb, this.queryOptions);
43
- return { rows, nextCursor: this.deriveNextCursor(rows, windowSize) };
44
- }
45
-
46
- private buildWindowQuery(cursor: CursorValue | null, windowSize: number): QueryBuilder<T> {
47
- const qb = this.createQuery();
48
- const primarySort = this.sort[0];
49
- if (cursor !== null) {
50
- qb.condition({
51
- field: primarySort.field,
52
- operator: primarySort.desc ? '<' : '>',
53
- value: cursor as T[keyof T],
54
- });
55
- }
56
- return qb.sort(this.sort).paginate({ start: 0, end: windowSize });
57
- }
58
-
59
- private deriveNextCursor(rows: T[], windowSize: number): CursorValue | null {
60
- // A short window means the data set is exhausted; a full one implies more may exist —
61
- // the next (short or empty) window settles it.
62
- if (rows.length < windowSize) {
63
- return null;
64
- }
65
- // NULL cursor-field rows collect at the tail under the sort, and a null tail can't
66
- // anchor a cursor: the data set's dated rows are exhausted, so paging stops here.
67
- const tail = rows[rows.length - 1][this.sort[0].field];
68
- return tail == null ? null : (tail as unknown as CursorValue);
43
+ const anchor = cursor === null ? null : CursorWindowPager.decodeAnchor(cursor as string);
44
+ const window = await this.pager.loadWindow(anchor, windowSize);
45
+ return {
46
+ rows: window.rows,
47
+ nextCursor: window.nextAnchor ? CursorWindowPager.encodeAnchor(window.nextAnchor) : null,
48
+ };
69
49
  }
70
50
  }
@@ -1,35 +1,24 @@
1
1
  /**
2
- * QueryCursorLoader (src/table/QueryCursorLoader.ts) at the pure level: react-query key
3
- * derivation, cursor threading into the window query, and next-cursor derivation — including
4
- * the null-tail exhaustion rule (NULL cursor-field rows collect at the tail under the sort;
5
- * a null tail can't anchor a cursor, so paging stops). Window queries are inspected through
6
- * the QueryBuilder graph; loadWindow's db round-trip is exercised with a stubbed getDb.
2
+ * QueryCursorLoader (src/table/QueryCursorLoader.ts) as the thin `CursorLoader` adapter over
3
+ * the db-owned `CursorWindowPager`: react-query key derivation, string-cursor encode/decode
4
+ * threading into the pager's lexicographic window queries (primary axis + `id` tiebreak), and
5
+ * exhaustion (short window, and the null-tail rule: NULL sort-value rows collect at the tail
6
+ * under the sort; a null can't anchor a continuation, so paging stops). The db round trip is
7
+ * injected through the pager's `options.db` seam via the house internals-cast pattern — no
8
+ * module mocking.
7
9
  */
8
10
  import moment from 'moment';
9
-
10
- const stagedRows: { rows: unknown[] } = { rows: [] };
11
- const queries: unknown[] = [];
12
- const stubDb = {
13
- query: async (_table: unknown, qb: unknown) => {
14
- queries.push(qb);
15
- return stagedRows.rows;
16
- },
17
- };
18
-
19
- jest.mock('@proteinjs/db', () => ({
20
- ...jest.requireActual('@proteinjs/db'),
21
- getDb: () => stubDb,
22
- }));
23
-
24
- import { QueryBuilder, SortCriteria, Table } from '@proteinjs/db';
25
- import { CursorValue } from '@proteinjs/ui';
11
+ import { Db, QueryBuilder, SortCriteria, Table } from '@proteinjs/db';
26
12
  import { QueryCursorLoader } from '../src/table/QueryCursorLoader';
13
+ // Load the reflection source graph so the house Serializer's custom serializers (moments)
14
+ // are registered — cursor encode/decode fidelity rides them.
15
+ import '../generated/index';
27
16
 
28
17
  type Row = {
29
18
  id: string;
30
19
  created: moment.Moment;
31
20
  updated: moment.Moment;
32
- lastActivityAt?: moment.Moment;
21
+ lastActivityAt?: moment.Moment | null;
33
22
  isPinned?: boolean;
34
23
  };
35
24
 
@@ -37,13 +26,23 @@ const table = { name: 'test_content' } as Table<Row>;
37
26
  const sort: SortCriteria<Row>[] = [{ field: 'lastActivityAt', desc: true }];
38
27
  const createQuery = () => new QueryBuilder<Row>(table.name);
39
28
 
40
- /** House pattern: private helpers reachable via a typed cast on the instance. */
41
- type LoaderInternals = {
42
- buildWindowQuery(cursor: CursorValue | null, windowSize: number): QueryBuilder<Row>;
43
- deriveNextCursor(rows: Row[], windowSize: number): CursorValue | null;
44
- };
29
+ const staged: { rows: Row[] } = { rows: [] };
30
+ const executed: QueryBuilder<Row>[] = [];
31
+ const stubDb = {
32
+ query: async (_table: unknown, qb: QueryBuilder<Row>) => {
33
+ executed.push(qb);
34
+ return staged.rows;
35
+ },
36
+ } as unknown as Db;
37
+
38
+ /** House pattern: private internals reachable via a typed cast on the instance. */
39
+ type LoaderInternals = { pager: { options?: { db?: Db } } };
45
40
 
46
- const internals = (loader: QueryCursorLoader<Row>) => loader as unknown as LoaderInternals;
41
+ const loaderWithStubDb = (sortCriteria?: SortCriteria<Row>[], query: () => QueryBuilder<Row> = createQuery) => {
42
+ const loader = new QueryCursorLoader<Row>(table, query, sortCriteria);
43
+ (loader as unknown as LoaderInternals).pager.options = { db: stubDb };
44
+ return loader;
45
+ };
47
46
 
48
47
  const graphNodes = (qb: QueryBuilder<Row>): any[] => qb.graph.nodes().map((id: string) => qb.graph.node(id));
49
48
  const conditionNodes = (qb: QueryBuilder<Row>) => graphNodes(qb).filter((node) => node?.type === 'CONDITION');
@@ -54,12 +53,12 @@ const dated = (id: string, at: string | null): Row => ({
54
53
  id,
55
54
  created: moment(),
56
55
  updated: moment(),
57
- lastActivityAt: at ? moment(at) : undefined,
56
+ lastActivityAt: at ? moment(at) : null,
58
57
  });
59
58
 
60
59
  beforeEach(() => {
61
- stagedRows.rows = [];
62
- queries.length = 0;
60
+ staged.rows = [];
61
+ executed.length = 0;
63
62
  });
64
63
 
65
64
  describe('reactQueryKeys', () => {
@@ -81,86 +80,80 @@ describe('reactQueryKeys', () => {
81
80
  });
82
81
  });
83
82
 
84
- describe('buildWindowQuery — cursor threading', () => {
85
- it('the first window carries no cursor condition, only the sort and the window pagination', () => {
86
- const loader = new QueryCursorLoader<Row>(table, createQuery, sort);
87
- const qb = internals(loader).buildWindowQuery(null, 30);
83
+ describe('loadWindow — cursor threading', () => {
84
+ it('the first window carries no cursor conditions the sort (with id tiebreak) and the window pagination', async () => {
85
+ const loader = loaderWithStubDb(sort);
86
+ staged.rows = [];
87
+ await loader.loadWindow(null, 30);
88
+ expect(executed).toHaveLength(1);
89
+ const qb = executed[0];
88
90
  expect(conditionNodes(qb)).toHaveLength(0);
89
91
  expect(paginationNode(qb)).toMatchObject({ start: 0, end: 30 });
90
- expect(sortNodes(qb)).toHaveLength(1);
91
- expect(sortNodes(qb)[0].criteria).toMatchObject({ field: 'lastActivityAt', desc: true });
92
- });
93
-
94
- it('a cursor becomes `field < cursor` on the primary sort field for a descending sort', () => {
95
- const loader = new QueryCursorLoader<Row>(table, createQuery, sort);
96
- const cursor = moment('2026-08-10T12:00:00.000Z');
97
- const qb = internals(loader).buildWindowQuery(cursor, 30);
98
- const conditions = conditionNodes(qb);
99
- expect(conditions).toHaveLength(1);
100
- expect(conditions[0]).toMatchObject({ field: 'lastActivityAt', operator: '<' });
101
- expect(conditions[0].value).toBe(cursor);
102
- expect(paginationNode(qb)).toMatchObject({ start: 0, end: 30 });
92
+ expect(sortNodes(qb).map((node) => node.criteria)).toEqual([
93
+ { field: 'lastActivityAt', desc: true },
94
+ { field: 'id', desc: false },
95
+ ]);
103
96
  });
104
97
 
105
- it('an ascending primary sort flips the cursor operator to `>`', () => {
106
- const loader = new QueryCursorLoader<Row>(table, createQuery, [{ field: 'lastActivityAt', desc: false }]);
107
- const qb = internals(loader).buildWindowQuery(5, 10);
108
- expect(conditionNodes(qb)[0]).toMatchObject({ field: 'lastActivityAt', operator: '>', value: 5 });
109
- });
110
-
111
- it('every window builds on a FRESH query — cursor conditions never accumulate across windows', () => {
112
- const loader = new QueryCursorLoader<Row>(table, createQuery, sort);
113
- internals(loader).buildWindowQuery(moment('2026-08-10T12:00:00.000Z'), 30);
114
- const second = internals(loader).buildWindowQuery(moment('2026-08-01T12:00:00.000Z'), 30);
115
- expect(conditionNodes(second)).toHaveLength(1);
116
- });
117
- });
118
-
119
- describe('deriveNextCursor — exhaustion', () => {
120
- const loader = new QueryCursorLoader<Row>(table, createQuery, sort);
121
-
122
- it('a short window is exhausted', () => {
123
- expect(internals(loader).deriveNextCursor([dated('a', '2026-08-10T12:00:00.000Z')], 3)).toBeNull();
124
- expect(internals(loader).deriveNextCursor([], 3)).toBeNull();
125
- });
126
-
127
- it('a full window anchors the next cursor at its tail', () => {
98
+ it('a full window returns a string cursor; feeding it back threads the lexicographic continuation', async () => {
99
+ const loader = loaderWithStubDb(sort);
128
100
  const rows = [
129
101
  dated('a', '2026-08-10T12:00:00.000Z'),
130
102
  dated('b', '2026-08-09T12:00:00.000Z'),
131
103
  dated('c', '2026-08-08T12:00:00.000Z'),
132
104
  ];
133
- const next = internals(loader).deriveNextCursor(rows, 3);
134
- expect(next).toBe(rows[2].lastActivityAt);
135
- });
136
-
137
- it('a full window with a NULL cursor-field tail is exhausted (null rows collect at the tail; no anchor)', () => {
138
- const rows = [dated('a', '2026-08-10T12:00:00.000Z'), dated('b', '2026-08-09T12:00:00.000Z'), dated('c', null)];
139
- expect(internals(loader).deriveNextCursor(rows, 3)).toBeNull();
105
+ staged.rows = rows;
106
+ const first = await loader.loadWindow(null, 3);
107
+ expect(first.rows).toBe(rows);
108
+ expect(typeof first.nextCursor).toBe('string');
109
+
110
+ staged.rows = [];
111
+ await loader.loadWindow(first.nextCursor, 3);
112
+ const qb = executed[1];
113
+ // Past-on-axis OR (equal-on-axis AND past-on-id), anchored at the tail row 'c' — with the
114
+ // moment surviving the string cursor round trip.
115
+ const conditions = conditionNodes(qb);
116
+ expect(conditions).toHaveLength(3);
117
+ const past = conditions.find((node) => node.operator === '<');
118
+ expect(past).toMatchObject({ field: 'lastActivityAt' });
119
+ expect(moment.isMoment(past.value)).toBe(true);
120
+ expect(past.value.toISOString()).toBe(rows[2].lastActivityAt!.toISOString());
121
+ expect(conditions.find((node) => node.operator === '=')).toMatchObject({ field: 'lastActivityAt' });
122
+ expect(conditions.find((node) => node.operator === '>')).toMatchObject({ field: 'id', value: 'c' });
140
123
  });
141
- });
142
124
 
143
- describe('loadWindow', () => {
144
- it('returns the queried rows with the derived next cursor', async () => {
145
- const loader = new QueryCursorLoader<Row>(table, createQuery, sort);
146
- const rows = [
125
+ it('every window builds on a FRESH query — cursor conditions never accumulate across windows', async () => {
126
+ const loader = loaderWithStubDb(sort);
127
+ staged.rows = [
147
128
  dated('a', '2026-08-10T12:00:00.000Z'),
148
129
  dated('b', '2026-08-09T12:00:00.000Z'),
149
130
  dated('c', '2026-08-08T12:00:00.000Z'),
150
131
  ];
151
- stagedRows.rows = rows;
132
+ const first = await loader.loadWindow(null, 3);
133
+ staged.rows = [
134
+ dated('d', '2026-08-07T12:00:00.000Z'),
135
+ dated('e', '2026-08-06T12:00:00.000Z'),
136
+ dated('f', '2026-08-05T12:00:00.000Z'),
137
+ ];
138
+ const second = await loader.loadWindow(first.nextCursor, 3);
139
+ staged.rows = [];
140
+ await loader.loadWindow(second.nextCursor, 3);
141
+ expect(conditionNodes(executed[2])).toHaveLength(3);
142
+ });
143
+ });
144
+
145
+ describe('loadWindow — exhaustion', () => {
146
+ it('a short window is exhausted (null cursor)', async () => {
147
+ const loader = loaderWithStubDb(sort);
148
+ staged.rows = [dated('a', '2026-08-10T12:00:00.000Z')];
152
149
  const window = await loader.loadWindow(null, 3);
153
- expect(window.rows).toBe(rows);
154
- expect(window.nextCursor).toBe(rows[2].lastActivityAt);
155
- // The executed query is the built window query: pagination sized to the window.
156
- expect(queries).toHaveLength(1);
157
- expect(paginationNode(queries[0] as QueryBuilder<Row>)).toMatchObject({ start: 0, end: 3 });
150
+ expect(window.nextCursor).toBeNull();
158
151
  });
159
152
 
160
- it('returns an exhausted window (null cursor) when the query comes back short', async () => {
161
- const loader = new QueryCursorLoader<Row>(table, createQuery, sort);
162
- stagedRows.rows = [dated('a', '2026-08-10T12:00:00.000Z')];
163
- const window = await loader.loadWindow(moment('2026-08-11T12:00:00.000Z'), 3);
153
+ it('a full window with a NULL sort-value tail is exhausted (null rows collect at the tail; no anchor)', async () => {
154
+ const loader = loaderWithStubDb(sort);
155
+ staged.rows = [dated('a', '2026-08-10T12:00:00.000Z'), dated('b', '2026-08-09T12:00:00.000Z'), dated('c', null)];
156
+ const window = await loader.loadWindow(null, 3);
164
157
  expect(window.nextCursor).toBeNull();
165
158
  });
166
159
  });
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import { renderToStaticMarkup } from 'react-dom/server';
3
+ import { recordFormPage } from '../src/pages/RecordFormPage';
4
+ import { recordTablePage } from '../src/pages/RecordTablePage';
5
+
6
+ describe('record form page error states', () => {
7
+ it('renders the not-accessible message for an unknown table (regression: a shadowed catch binding swallowed the message and the page rendered blank)', () => {
8
+ const Form = recordFormPage.component;
9
+ const html = renderToStaticMarkup(<Form urlParams={{ table: 'NotARealTable' }} />);
10
+ expect(html).toContain('Table not accessible in UI: NotARealTable');
11
+ });
12
+
13
+ it('renders the missing-param message when no table is provided', () => {
14
+ const Form = recordFormPage.component;
15
+ const html = renderToStaticMarkup(<Form urlParams={{}} />);
16
+ // (apostrophes render HTML-escaped in static markup, so assert around them)
17
+ expect(html).toContain('Table name not provided via the');
18
+ });
19
+ });
20
+
21
+ describe('record table page error states', () => {
22
+ it('renders the not-accessible message outside the table card (the stretched Paper clipped it)', () => {
23
+ const Table = recordTablePage.component;
24
+ const html = renderToStaticMarkup(<Table urlParams={{ name: 'NotARealTable' }} />);
25
+ expect(html).toContain('Table not accessible in UI: NotARealTable');
26
+ expect(html).not.toContain('MuiPaper');
27
+ });
28
+
29
+ it('renders the missing-param message when no name is provided', () => {
30
+ const Table = recordTablePage.component;
31
+ const html = renderToStaticMarkup(<Table urlParams={{}} />);
32
+ // (apostrophes render HTML-escaped in static markup, so assert around them)
33
+ expect(html).toContain('Table not provided via the');
34
+ });
35
+ });