diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2b2c09d796d..4b9ae0f3e2c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -314,7 +314,7 @@ jobs: DATABASE_URL: 'file:./test.db' strategy: matrix: - test: ['init.test.ts', 'list-view-crud.test.ts'] + test: ['init.test.ts', 'list-view-crud.test.ts', 'navigation.test.ts'] fail-fast: false steps: - name: Checkout Repo diff --git a/tests/admin-ui-tests/init.test.ts b/tests/admin-ui-tests/init.test.ts index d5981bf5709..148ee09abaa 100644 --- a/tests/admin-ui-tests/init.test.ts +++ b/tests/admin-ui-tests/init.test.ts @@ -13,6 +13,16 @@ adminUITests('./tests/test-projects/basic', browserType => { test('Task List card should be visible', async () => { await page.waitForSelector('h3:has-text("Task")'); }); + test('Clicking on the logo should return you to the Dashboard route', async () => { + await page.goto('http://localhost:3000/tasks'); + await page.waitForSelector('h3 a:has-text("Keystone 6")'); + await Promise.all([ + page.waitForNavigation({ + url: 'http://localhost:3000', + }), + page.click('h3 a:has-text("Keystone 6")'), + ]); + }); test('Should see a 404 on request of the /init route', async () => { await page.goto('http://localhost:3000/init'); const content = await page.textContent('body h1'); diff --git a/tests/admin-ui-tests/list-view-crud.test.ts b/tests/admin-ui-tests/list-view-crud.test.ts index c174de5e7a4..358175bd464 100644 --- a/tests/admin-ui-tests/list-view-crud.test.ts +++ b/tests/admin-ui-tests/list-view-crud.test.ts @@ -1,30 +1,10 @@ import { Browser, Page } from 'playwright'; -import fetch from 'node-fetch'; -import { adminUITests, deleteAllData } from './utils'; + +import { makeGqlRequest, adminUITests, deleteAllData } from './utils'; adminUITests('./tests/test-projects/crud-notifications', browserType => { let browser: Browser = undefined as any; let page: Page = undefined as any; - const seedData = async (query: string, variables?: Record) => { - try { - const { errors } = await fetch('http://localhost:3000/api/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - query, - variables, - }), - }).then(res => res.json()); - if (errors) { - throw errors; - } - } catch (e) { - console.log(e); - throw e; - } - }; beforeAll(async () => { browser = await browserType.launch(); @@ -46,7 +26,7 @@ adminUITests('./tests/test-projects/crud-notifications', browserType => { } } `; - await seedData(query); + await makeGqlRequest(query); await Promise.all([page.waitForNavigation(), page.goto('http://localhost:3000/tasks')]); await page.waitForSelector('tbody tr:first-of-type td:first-of-type label'); await page.click('tbody tr:first-of-type td:first-of-type label'); @@ -71,7 +51,7 @@ adminUITests('./tests/test-projects/crud-notifications', browserType => { } } `; - await seedData(query); + await makeGqlRequest(query); await Promise.all([page.waitForNavigation(), page.goto('http://localhost:3000/tasks')]); await page.click('tbody tr:first-of-type td:first-of-type label'); await page.click('button:has-text("Delete")'); @@ -105,7 +85,7 @@ adminUITests('./tests/test-projects/crud-notifications', browserType => { } }), }; - await seedData(query, variables); + await makeGqlRequest(query, variables); await Promise.all([ page.waitForNavigation(), page.goto('http://localhost:3000/tasks?sortBy=label&page=1'), diff --git a/tests/admin-ui-tests/navigation.test.ts b/tests/admin-ui-tests/navigation.test.ts new file mode 100644 index 00000000000..b1650fcf505 --- /dev/null +++ b/tests/admin-ui-tests/navigation.test.ts @@ -0,0 +1,75 @@ +import { Browser, Page } from 'playwright'; +import { adminUITests } from './utils'; + +adminUITests('./tests/test-projects/basic', browserType => { + let browser: Browser = undefined as any; + let page: Page = undefined as any; + + beforeAll(async () => { + browser = await browserType.launch(); + page = await browser.newPage(); + await page.goto('http://localhost:3000'); + }); + test('Nav contains a Dashboard route by default', async () => { + await page.waitForSelector('nav a:has-text("Dashboard")'); + }); + test('When at the index, the Dashboard NavItem is selected', async () => { + const element = await page.waitForSelector('nav a:has-text("Dashboard")'); + const ariaCurrent = await element?.getAttribute('aria-current'); + expect(ariaCurrent).toBe('location'); + }); + test('When navigated to a List route, the representative list NavItem is selected', async () => { + await page.goto('http://localhost:3000/tasks'); + const element = await page.waitForSelector('nav a:has-text("Tasks")'); + const ariaCurrent = await element?.getAttribute('aria-current'); + expect(ariaCurrent).toBe('location'); + }); + test('Can access all list pages via the navigation', async () => { + await page.goto('http://localhost:3000'); + await Promise.all([ + page.waitForNavigation({ + url: 'http://localhost:3000/tasks', + }), + page.click('nav a:has-text("Tasks")'), + ]); + await Promise.all([ + page.waitForNavigation({ + url: 'http://localhost:3000/people', + }), + page.click('nav a:has-text("People")'), + ]); + }); + test('Can not access hidden lists via the navigation', async () => { + await Promise.all([page.waitForNavigation(), page.goto('http://localhost:3000')]); + await page.waitForSelector('nav'); + const navItems = await page.$$('nav li a'); + const navLinks = await Promise.all( + navItems.map(navItem => { + return navItem.getAttribute('href'); + }) + ); + expect(navLinks.length).toBe(3); + expect(navLinks.includes('/secretplans')).toBe(false); + }); + test('When navigated to an Item view, the representative list NavItem is selected', async () => { + await page.goto('http://localhost:3000'); + await page.click('button[title="Create Task"]'); + await page.fill('id=label', 'Test Task'); + await Promise.all([page.waitForNavigation(), page.click('button[type="submit"]')]); + const element = await page.waitForSelector('nav a:has-text("Tasks")'); + const ariaCurrent = await element?.getAttribute('aria-current'); + expect(ariaCurrent).toBe('location'); + }); + test('When pressing a list view nav item from an item view, the correct route should be reached', async () => { + await page.goto('http://localhost:3000'); + await page.click('button[title="Create Task"]'); + await page.fill('id=label', 'Test Task'); + await Promise.all([page.waitForNavigation(), page.click('button[type="submit"]')]); + await Promise.all([page.waitForNavigation(), page.click('nav a:has-text("Tasks")')]); + + expect(page.url()).toBe('http://localhost:3000/tasks'); + }); + afterAll(async () => { + await browser.close(); + }); +}); diff --git a/tests/admin-ui-tests/utils.ts b/tests/admin-ui-tests/utils.ts index 48c38a31494..d0f8d85caae 100644 --- a/tests/admin-ui-tests/utils.ts +++ b/tests/admin-ui-tests/utils.ts @@ -1,6 +1,7 @@ import path from 'path'; import fs from 'fs'; import { promisify } from 'util'; +import fetch from 'node-fetch'; import execa from 'execa'; import _treeKill from 'tree-kill'; import * as playwright from 'playwright'; @@ -21,6 +22,23 @@ const promiseSignal = (): Promise & { resolve: () => void } => { }; const projectRoot = findRootSync(process.cwd()); +export const makeGqlRequest = async (query: string, variables?: Record) => { + const { errors } = await fetch('http://localhost:3000/api/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + variables, + }), + }).then(res => res.json()); + + if (errors) { + throw new Error(`graphql errors: ${errors.map((x: Error) => x.message).join('\n')}`); + } +}; + export const deleteAllData: (projectDir: string) => Promise = async (projectDir: string) => { const { PrismaClient } = require(path.resolve( projectRoot, diff --git a/tests/test-projects/basic/schema.graphql b/tests/test-projects/basic/schema.graphql index 24200845cd6..4c33e8d5145 100644 --- a/tests/test-projects/basic/schema.graphql +++ b/tests/test-projects/basic/schema.graphql @@ -202,6 +202,42 @@ input TaskRelateToManyForCreateInput { connect: [TaskWhereUniqueInput!] } +type SecretPlan { + id: ID! + label: String + description: String +} + +input SecretPlanWhereUniqueInput { + id: ID +} + +input SecretPlanWhereInput { + AND: [SecretPlanWhereInput!] + OR: [SecretPlanWhereInput!] + NOT: [SecretPlanWhereInput!] + id: IDFilter +} + +input SecretPlanOrderByInput { + id: OrderDirection +} + +input SecretPlanUpdateInput { + label: String + description: String +} + +input SecretPlanUpdateArgs { + where: SecretPlanWhereUniqueInput! + data: SecretPlanUpdateInput! +} + +input SecretPlanCreateInput { + label: String + description: String +} + """ The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). """ @@ -223,6 +259,15 @@ type Mutation { updatePeople(data: [PersonUpdateArgs!]!): [Person] deletePerson(where: PersonWhereUniqueInput!): Person deletePeople(where: [PersonWhereUniqueInput!]!): [Person] + createSecretPlan(data: SecretPlanCreateInput!): SecretPlan + createSecretPlans(data: [SecretPlanCreateInput!]!): [SecretPlan] + updateSecretPlan( + where: SecretPlanWhereUniqueInput! + data: SecretPlanUpdateInput! + ): SecretPlan + updateSecretPlans(data: [SecretPlanUpdateArgs!]!): [SecretPlan] + deleteSecretPlan(where: SecretPlanWhereUniqueInput!): SecretPlan + deleteSecretPlans(where: [SecretPlanWhereUniqueInput!]!): [SecretPlan] } type Query { @@ -242,6 +287,14 @@ type Query { ): [Person!] person(where: PersonWhereUniqueInput!): Person peopleCount(where: PersonWhereInput! = {}): Int + secretPlans( + where: SecretPlanWhereInput! = {} + orderBy: [SecretPlanOrderByInput!]! = [] + take: Int + skip: Int! = 0 + ): [SecretPlan!] + secretPlan(where: SecretPlanWhereUniqueInput!): SecretPlan + secretPlansCount(where: SecretPlanWhereInput! = {}): Int keystone: KeystoneMeta! } diff --git a/tests/test-projects/basic/schema.prisma b/tests/test-projects/basic/schema.prisma index 78461ee962d..981c3d6d1fb 100644 --- a/tests/test-projects/basic/schema.prisma +++ b/tests/test-projects/basic/schema.prisma @@ -27,4 +27,10 @@ model Person { id String @id @default(cuid()) name String? tasks Task[] @relation("Task_assignedTo") +} + +model SecretPlan { + id String @id @default(cuid()) + label String? + description String? } \ No newline at end of file diff --git a/tests/test-projects/basic/schema.ts b/tests/test-projects/basic/schema.ts index d76a4cb034d..13e09ba4ffc 100644 --- a/tests/test-projects/basic/schema.ts +++ b/tests/test-projects/basic/schema.ts @@ -29,4 +29,13 @@ export const lists = createSchema({ defaultIsFilterable: true, defaultIsOrderable: true, }), + SecretPlan: list({ + fields: { + label: text(), + description: text(), + }, + ui: { + isHidden: true, + }, + }), });