mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
console (tests): interaction tests for Native Query relationships
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9831 Co-authored-by: Matthew Goodwin <49927862+m4ttheweric@users.noreply.github.com> GitOrigin-RevId: cf488f5cc20ab77156f1e0c70ba830c3d5b6f495
This commit is contained in:
parent
4117030e09
commit
ad64379876
@ -1,34 +1,157 @@
|
||||
import { StoryObj, Meta } from '@storybook/react';
|
||||
import { ReactQueryDecorator } from '../../../../../storybook/decorators/react-query';
|
||||
import { ListNativeQueryRelationships } from './ListNativeQueryRelationships';
|
||||
import {
|
||||
ListNativeQueryRelationships,
|
||||
ListNativeQueryRow,
|
||||
} from './ListNativeQueryRelationships';
|
||||
import { ReduxDecorator } from '../../../../../storybook/decorators/redux-decorator';
|
||||
import { nativeQueryHandlers } from '../../AddNativeQuery/mocks';
|
||||
import { handlers } from '../mocks/handlers';
|
||||
import globals from '../../../../../Globals';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
component: ListNativeQueryRelationships,
|
||||
decorators: [
|
||||
ReactQueryDecorator(),
|
||||
ReduxDecorator({
|
||||
tables: {},
|
||||
tables: {
|
||||
dataHeaders: {
|
||||
'x-hasura-admin-secret': globals.adminSecret as any,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
msw: nativeQueryHandlers({
|
||||
metadataOptions: { postgres: { models: true, queries: true } },
|
||||
trackNativeQueryResult: 'success',
|
||||
}),
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
argTypes: {
|
||||
onDeleteRow: { action: 'clicked delete' },
|
||||
onEditRow: { action: 'clicked edit' },
|
||||
},
|
||||
} as Meta<typeof ListNativeQueryRelationships>;
|
||||
|
||||
export const DefaultView: StoryObj<typeof ListNativeQueryRelationships> = {
|
||||
args: {
|
||||
dataSourceName: 'postgres',
|
||||
nativeQueryName: 'customer_native_query',
|
||||
export const Basic: StoryObj<typeof ListNativeQueryRelationships> = {
|
||||
render: () => (
|
||||
<ListNativeQueryRelationships
|
||||
dataSourceName="chinook"
|
||||
nativeQueryName="get_authors"
|
||||
/>
|
||||
),
|
||||
parameters: {
|
||||
msw: handlers(),
|
||||
},
|
||||
};
|
||||
|
||||
export const TestBasicFlow: StoryObj<typeof ListNativeQueryRelationships> = {
|
||||
render: () => {
|
||||
const [result, updateResult] = useState<ListNativeQueryRow>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ListNativeQueryRelationships
|
||||
dataSourceName="chinook"
|
||||
nativeQueryName="get_authors"
|
||||
onEditRow={data => updateResult(data)}
|
||||
onDeleteRow={data => updateResult(data)}
|
||||
/>
|
||||
<div data-testid="result">{JSON.stringify(result)}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
parameters: {
|
||||
msw: handlers(),
|
||||
},
|
||||
name: '🧪 Basic render and edit/delete action',
|
||||
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const nativeQueryRelationshipsTable = await canvas.findByTestId(
|
||||
'native-query-relationships'
|
||||
);
|
||||
|
||||
await expect(nativeQueryRelationshipsTable).toBeInTheDocument();
|
||||
|
||||
/**
|
||||
* Check if both header and body have rendered
|
||||
*/
|
||||
await expect(nativeQueryRelationshipsTable.children.length).toEqual(2);
|
||||
|
||||
const rows = await canvas.findAllByTestId(
|
||||
/^native-query-relationships-row-.*$/
|
||||
);
|
||||
/**
|
||||
* There should be two rows
|
||||
*/
|
||||
await expect(rows.length).toEqual(2);
|
||||
let rowValues = await canvas.findAllByTestId(
|
||||
/^native-query-relationships-cell-0-*.*$/
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify the row values
|
||||
*/
|
||||
await expect(rowValues[0]).toHaveTextContent('articles');
|
||||
await expect(rowValues[1]).toHaveTextContent('array');
|
||||
|
||||
let editButton = await within(rowValues[2]).findByTestId('edit-button');
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await expect(await canvas.getByTestId('result')).toHaveTextContent(
|
||||
JSON.stringify({
|
||||
name: 'articles',
|
||||
using: {
|
||||
column_mapping: { id: 'author_id' },
|
||||
insertion_order: null,
|
||||
remote_native_query: 'get_article',
|
||||
},
|
||||
type: 'array',
|
||||
})
|
||||
);
|
||||
|
||||
let deleteBtn = await within(rowValues[2]).findByTestId('delete-button');
|
||||
await userEvent.click(deleteBtn);
|
||||
await expect(await canvas.getByTestId('result')).toHaveTextContent(
|
||||
JSON.stringify({
|
||||
name: 'articles',
|
||||
using: {
|
||||
column_mapping: { id: 'author_id' },
|
||||
insertion_order: null,
|
||||
remote_native_query: 'get_article',
|
||||
},
|
||||
type: 'array',
|
||||
})
|
||||
);
|
||||
|
||||
rowValues = await canvas.findAllByTestId(
|
||||
/^native-query-relationships-cell-1-*.*$/
|
||||
);
|
||||
|
||||
await expect(rowValues[0]).toHaveTextContent('author_details');
|
||||
await expect(rowValues[1]).toHaveTextContent('object');
|
||||
|
||||
editButton = await within(rowValues[2]).findByTestId('edit-button');
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await expect(await canvas.getByTestId('result')).toHaveTextContent(
|
||||
JSON.stringify({
|
||||
name: 'author_details',
|
||||
using: {
|
||||
column_mapping: { id: 'author_id' },
|
||||
insertion_order: null,
|
||||
remote_native_query: 'get_author_details',
|
||||
},
|
||||
type: 'object',
|
||||
})
|
||||
);
|
||||
|
||||
deleteBtn = await within(rowValues[2]).findByTestId('delete-button');
|
||||
await userEvent.click(deleteBtn);
|
||||
await expect(await canvas.getByTestId('result')).toHaveTextContent(
|
||||
JSON.stringify({
|
||||
name: 'author_details',
|
||||
using: {
|
||||
column_mapping: { id: 'author_id' },
|
||||
insertion_order: null,
|
||||
remote_native_query: 'get_author_details',
|
||||
},
|
||||
type: 'object',
|
||||
})
|
||||
);
|
||||
},
|
||||
render: args => <ListNativeQueryRelationships {...args} />,
|
||||
};
|
||||
|
@ -75,6 +75,7 @@ export const ListNativeQueryRelationships = (
|
||||
onClick={() => {
|
||||
onEditRow?.(row.original);
|
||||
}}
|
||||
data-testid="edit-button"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
@ -84,6 +85,7 @@ export const ListNativeQueryRelationships = (
|
||||
onClick={() => {
|
||||
onDeleteRow?.(row.original);
|
||||
}}
|
||||
data-testid="delete-button"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
@ -104,13 +106,14 @@ export const ListNativeQueryRelationships = (
|
||||
const NativeQueryRelationshipsTable =
|
||||
useCardedTableFromReactTableWithRef<ListNativeQueryRow>();
|
||||
|
||||
if (isLoading) return <Skeleton count={10} />;
|
||||
if (isLoading) return <Skeleton count={10} height={20} />;
|
||||
|
||||
return (
|
||||
<NativeQueryRelationshipsTable
|
||||
table={relationshipsTable}
|
||||
ref={tableRef}
|
||||
noRowsMessage={'No relationships added'}
|
||||
dataTestId="native-query-relationships"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,120 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ReactQueryDecorator } from '../../../../../storybook/decorators/react-query';
|
||||
import { ReduxDecorator } from '../../../../../storybook/decorators/redux-decorator';
|
||||
import { NativeQueryRelationshipWidget } from './NativeQueryRelationshipWidget';
|
||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { handlers } from '../mocks/handlers';
|
||||
import { useState } from 'react';
|
||||
import { NativeQueryRelationshipFormSchema } from '../schema';
|
||||
|
||||
export default {
|
||||
component: NativeQueryRelationshipWidget,
|
||||
decorators: [
|
||||
ReactQueryDecorator(),
|
||||
ReduxDecorator({
|
||||
tables: {
|
||||
dataHeaders: {
|
||||
'x-hasura-admin-secret': 'myadminsecretkey' as any,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
} as Meta<typeof NativeQueryRelationshipWidget>;
|
||||
|
||||
export const DefaultView: StoryObj<typeof NativeQueryRelationshipWidget> = {
|
||||
render: () => {
|
||||
return (
|
||||
<NativeQueryRelationshipWidget
|
||||
fromNativeQuery="get_authors"
|
||||
dataSourceName="chinook"
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const TestBasicInteraction: StoryObj<
|
||||
typeof NativeQueryRelationshipWidget
|
||||
> = {
|
||||
render: () => {
|
||||
const [formValues, setFormValues] =
|
||||
useState<NativeQueryRelationshipFormSchema>();
|
||||
return (
|
||||
<div>
|
||||
<NativeQueryRelationshipWidget
|
||||
fromNativeQuery="get_authors"
|
||||
dataSourceName="chinook"
|
||||
onSubmit={data => setFormValues(data)}
|
||||
/>
|
||||
<div data-testid="result">{JSON.stringify(formValues)}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
parameters: {
|
||||
msw: handlers(),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await waitFor(
|
||||
async () => {
|
||||
return await expect(
|
||||
canvas.getByLabelText('Relationship Name')
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
{
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
// console.log(await canvas.getByLabelText('Relationship Name'));
|
||||
await userEvent.type(
|
||||
await canvas.getByLabelText('Relationship Name'),
|
||||
'articles'
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
await canvas.getByLabelText('Target Native Query'),
|
||||
'get_article'
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
await canvas.getByLabelText('Relationship Type'),
|
||||
'array'
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
async () => {
|
||||
return await expect(
|
||||
canvas.getByTestId('columnMapping_source_input_0')
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
{
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
await canvas.getByTestId('columnMapping_source_input_0'),
|
||||
'id'
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
await canvas.getByTestId('columnMapping_target_input_0'),
|
||||
'author_id'
|
||||
);
|
||||
|
||||
await userEvent.click(canvas.getByText('Add Relationship'));
|
||||
|
||||
await waitFor(async () => {
|
||||
return await expect(await canvas.getByTestId('result')).toHaveTextContent(
|
||||
JSON.stringify({
|
||||
name: 'articles',
|
||||
toNativeQuery: 'get_article',
|
||||
type: 'array',
|
||||
columnMapping: { id: 'author_id' },
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
@ -28,6 +28,7 @@ export const TrackNativeQueryRelationshipForm = ({
|
||||
label="Relationship Name"
|
||||
placeholder="Name your native query relationship"
|
||||
name={'name'}
|
||||
dataTestId="relationship_name"
|
||||
/>
|
||||
<Select
|
||||
name={'toNativeQuery'}
|
||||
@ -50,11 +51,13 @@ export const TrackNativeQueryRelationshipForm = ({
|
||||
from={{
|
||||
options: fromFieldOptions,
|
||||
label: 'Source Field',
|
||||
placeholder: 'Pick source field',
|
||||
}}
|
||||
to={{
|
||||
type: 'array',
|
||||
options: toFieldOptions,
|
||||
label: 'Target Field',
|
||||
placeholder: 'Pick target field',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -0,0 +1,16 @@
|
||||
import { rest } from 'msw';
|
||||
import { mockMetadata } from './mockData';
|
||||
|
||||
const baseUrl = 'http://localhost:8080';
|
||||
|
||||
export const handlers = (url = baseUrl) => [
|
||||
rest.post(`${url}/v1/metadata`, async (_req, res, ctx) => {
|
||||
const reqBody = (await _req.json()) as Record<string, any>;
|
||||
|
||||
if (reqBody.type === 'export_metadata') return res(ctx.json(mockMetadata));
|
||||
|
||||
console.log(reqBody.type);
|
||||
|
||||
return res(ctx.json({}));
|
||||
}),
|
||||
];
|
@ -0,0 +1,135 @@
|
||||
import { Metadata } from '../../../../hasura-metadata-types';
|
||||
|
||||
export const mockMetadata: Metadata = {
|
||||
resource_version: 24,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'chinook',
|
||||
kind: 'postgres',
|
||||
tables: [],
|
||||
native_queries: [
|
||||
{
|
||||
arguments: {
|
||||
shouting_title: {
|
||||
nullable: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
code: 'SELECT id, author_id, (CASE WHEN {{shouting_title}}=true THEN UPPER(title) ELSE title END) as title, content FROM article',
|
||||
returns: 'article_model',
|
||||
root_field_name: 'get_article',
|
||||
},
|
||||
{
|
||||
arguments: {
|
||||
shouting_name: {
|
||||
nullable: false,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author_details',
|
||||
using: {
|
||||
column_mapping: {
|
||||
id: 'author_id',
|
||||
},
|
||||
insertion_order: null,
|
||||
remote_native_query: 'get_author_details',
|
||||
},
|
||||
},
|
||||
],
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'articles',
|
||||
using: {
|
||||
column_mapping: {
|
||||
id: 'author_id',
|
||||
},
|
||||
insertion_order: null,
|
||||
remote_native_query: 'get_article',
|
||||
},
|
||||
},
|
||||
],
|
||||
code: 'SELECT id, (CASE WHEN {{shouting_name}}=true THEN UPPER(name) ELSE name END) as name FROM author',
|
||||
returns: 'author_model',
|
||||
root_field_name: 'get_authors',
|
||||
},
|
||||
],
|
||||
logical_models: [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: {
|
||||
nullable: false,
|
||||
scalar: 'integer',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'author_id',
|
||||
type: {
|
||||
nullable: false,
|
||||
scalar: 'integer',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: {
|
||||
nullable: false,
|
||||
scalar: 'text',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: {
|
||||
nullable: false,
|
||||
scalar: 'text',
|
||||
},
|
||||
},
|
||||
],
|
||||
name: 'article_model',
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'id',
|
||||
type: {
|
||||
nullable: false,
|
||||
scalar: 'integer',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: {
|
||||
nullable: false,
|
||||
scalar: 'text',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'articles',
|
||||
type: {
|
||||
array: {
|
||||
logical_model: 'article_model',
|
||||
nullable: false,
|
||||
},
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
name: 'author_model',
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url:
|
||||
'postgres://postgres:test@host.docker.internal:6001/chinook',
|
||||
isolation_level: 'read-committed',
|
||||
use_prepared_statements: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -5,15 +5,20 @@ import React from 'react';
|
||||
export function CardedTableFromReactTable<T>({
|
||||
table,
|
||||
noRowsMessage,
|
||||
dataTestId,
|
||||
}: {
|
||||
table: Table<T>;
|
||||
noRowsMessage?: string;
|
||||
dataTestId?: string;
|
||||
}) {
|
||||
return (
|
||||
<CardedTable.Table>
|
||||
<CardedTable.Table data-testid={`${dataTestId}`}>
|
||||
<CardedTable.TableHead>
|
||||
{table.getHeaderGroups().map(headerGroup => (
|
||||
<CardedTable.TableHeadRow key={headerGroup.id}>
|
||||
{table.getHeaderGroups().map((headerGroup, index) => (
|
||||
<CardedTable.TableHeadRow
|
||||
key={headerGroup.id}
|
||||
data-testid={`${dataTestId}-header-${index}`}
|
||||
>
|
||||
{headerGroup.headers.map(header => (
|
||||
<CardedTable.TableHeadCell key={header.id}>
|
||||
{header.isPlaceholder
|
||||
@ -28,10 +33,16 @@ export function CardedTableFromReactTable<T>({
|
||||
))}
|
||||
</CardedTable.TableHead>
|
||||
<CardedTable.TableBody>
|
||||
{table.getRowModel().rows.map(row => (
|
||||
<CardedTable.TableBodyRow key={row.id}>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<CardedTable.TableBodyCell key={cell.id}>
|
||||
{table.getRowModel().rows.map((row, index) => (
|
||||
<CardedTable.TableBodyRow
|
||||
key={row.id}
|
||||
data-testid={`${dataTestId}-row-${index}`}
|
||||
>
|
||||
{row.getVisibleCells().map((cell, subIndex) => (
|
||||
<CardedTable.TableBodyCell
|
||||
key={cell.id}
|
||||
data-testid={`${dataTestId}-cell-${index}-${subIndex}`}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</CardedTable.TableBodyCell>
|
||||
))}
|
||||
@ -60,13 +71,14 @@ export function CardedTableFromReactTable<T>({
|
||||
function forwardRefWrapper<T>() {
|
||||
return React.forwardRef<
|
||||
HTMLDivElement,
|
||||
{ table: Table<T>; noRowsMessage?: string }
|
||||
>(({ table, noRowsMessage }, ref) => {
|
||||
{ table: Table<T>; noRowsMessage?: string; dataTestId?: string }
|
||||
>(({ table, noRowsMessage, dataTestId }, ref) => {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<CardedTableFromReactTable
|
||||
table={table}
|
||||
noRowsMessage={noRowsMessage}
|
||||
dataTestId={dataTestId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -179,7 +179,7 @@ export const buildMetadata = ({
|
||||
kind: 'mssql',
|
||||
tables: [],
|
||||
native_queries: mssql?.queries ? testQueries.mssql : [],
|
||||
logical_models: mssql?.models ? testModels.mssql : [],
|
||||
logical_models: mssql?.models ? (testModels.mssql as any) : [],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
connection_string: '',
|
||||
@ -191,7 +191,7 @@ export const buildMetadata = ({
|
||||
kind: 'postgres',
|
||||
tables: [],
|
||||
native_queries: postgres?.queries ? testQueries.postgres : [],
|
||||
logical_models: postgres?.models ? testModels.postgres : [],
|
||||
logical_models: postgres?.models ? (testModels.postgres as any) : [],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: '',
|
||||
|
Loading…
Reference in New Issue
Block a user