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:
Vijay Prasanna 2023-07-18 09:57:22 +05:30 committed by hasura-bot
parent 4117030e09
commit ad64379876
8 changed files with 443 additions and 31 deletions

View File

@ -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} />,
};

View File

@ -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"
/>
);
};

View File

@ -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' },
})
);
});
},
};

View File

@ -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>

View File

@ -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({}));
}),
];

View File

@ -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,
},
},
},
],
},
};

View File

@ -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>
);

View File

@ -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: '',