diff --git a/packages/bruno-tests/src/graphql/index.js b/packages/bruno-tests/src/graphql/index.js index 7e5001b4c..cad18e2d2 100644 --- a/packages/bruno-tests/src/graphql/index.js +++ b/packages/bruno-tests/src/graphql/index.js @@ -5,12 +5,34 @@ const setupGraphQL = async (app) => { const yoga = createYoga({ schema: createSchema({ typeDefs: /* GraphQL */ ` + type User { + id: ID! + name: String! + email: String! + age: Int + posts: [Post!]! + } + + type Post { + id: ID! + title: String! + body: String! + author: User! + comments: [Comment!]! + } + + type Comment { + id: ID! + text: String! + author: User! + } + type Company { ceo: String name: String founder: String } - + input ICreate { id: String! } @@ -19,12 +41,40 @@ const setupGraphQL = async (app) => { success: Boolean } + union SearchResult = User | Post + + input CreateUserInput { + name: String! + email: String! + age: Int + } + + input UpdateUserInput { + name: String + email: String + age: Int + } + + input CreatePostInput { + title: String! + body: String! + authorId: ID! + } + type Query { company: Company + user(id: ID!): User + users(limit: Int, offset: Int): [User!]! + post(id: ID!): Post + search(term: String!): [SearchResult!]! } - + type Mutation { create(payload: ICreate!): Message + createUser(input: CreateUserInput!): User! + updateUser(id: ID!, input: UpdateUserInput!): User + deleteUser(id: ID!): Boolean! + createPost(input: CreatePostInput!): Post! } `, resolvers: { @@ -33,12 +83,54 @@ const setupGraphQL = async (app) => { ceo: 'Elon Musk', name: 'SpaceX', founder: 'Elon Musk' - }) + }), + user: (_parent, args) => ({ + id: args.id, + name: 'John Doe', + email: 'john@example.com', + age: 30, + posts: [] + }), + users: () => [ + { id: '1', name: 'John Doe', email: 'john@example.com', age: 30, posts: [] }, + { id: '2', name: 'Jane Smith', email: 'jane@example.com', age: 25, posts: [] } + ], + post: (_parent, args) => ({ + id: args.id, + title: 'Test Post', + body: 'Post body', + author: { id: '1', name: 'John Doe', email: 'john@example.com', age: 30, posts: [] }, + comments: [] + }), + search: () => [] }, Mutation: { create: () => ({ success: true + }), + createUser: (_parent, { input }) => ({ + id: '3', + ...input, + posts: [] + }), + updateUser: (_parent, { id, input }) => ({ + id, + name: input.name || 'John Doe', + email: input.email || 'john@example.com', + age: input.age || 30, + posts: [] + }), + deleteUser: () => true, + createPost: (_parent, { input }) => ({ + id: '1', + title: input.title, + body: input.body, + author: { id: input.authorId, name: 'John Doe', email: 'john@example.com', age: 30, posts: [] }, + comments: [] }) + }, + SearchResult: { + __resolveType: (obj) => (obj.email ? 'User' : 'Post') } } }), diff --git a/tests/graphql/query-builder/fixtures/collection/test-graphql.yml b/tests/graphql/query-builder/fixtures/collection/test-graphql.yml index fbf879915..c53d8b6d9 100644 --- a/tests/graphql/query-builder/fixtures/collection/test-graphql.yml +++ b/tests/graphql/query-builder/fixtures/collection/test-graphql.yml @@ -5,7 +5,7 @@ info: graphql: method: POST - url: https://graphql.anilist.co + url: http://localhost:8081/api/graphql body: query: |- diff --git a/tests/graphql/query-builder/query-builder.spec.ts b/tests/graphql/query-builder/query-builder.spec.ts index 30ceb234a..80df9ba67 100644 --- a/tests/graphql/query-builder/query-builder.spec.ts +++ b/tests/graphql/query-builder/query-builder.spec.ts @@ -49,34 +49,34 @@ test.describe('GraphQL Query Builder', () => { const introspectionItem = page.locator('[data-testid="menu-dropdown-schema-introspection"]'); await introspectionItem.waitFor({ state: 'visible' }); await introspectionItem.click(); - await expect(page.getByText('GraphQL Schema loaded successfully').first()).toBeVisible({ timeout: 15000 }); + await expect(page.getByText('GraphQL Schema loaded successfully').first()).toBeVisible(); await expect(qb(page).locator('.query-builder-tree')).toBeVisible(); }); - await test.step('Click on "Media" field to expand it', async () => { - const mediaField = qb(page).locator('.field-node').filter({ hasText: /^Media/ }).first(); - await mediaField.click(); + await test.step('Click on "users" field to expand it', async () => { + const usersField = qb(page).locator('.field-node').filter({ hasText: /^users/ }).first(); + await usersField.click(); await expect(qb(page).locator('.section-header').filter({ hasText: 'ARGUMENTS' }).first()).toBeVisible(); }); - await test.step('Check the "Media" field checkbox', async () => { - const mediaCheckbox = qb(page).locator('.field-node').filter({ hasText: /^Media/ }).first().locator('.field-checkbox'); - await mediaCheckbox.check(); - await expect(mediaCheckbox).toBeChecked(); + await test.step('Check the "users" field checkbox', async () => { + const usersCheckbox = qb(page).locator('.field-node').filter({ hasText: /^users/ }).first().locator('.field-checkbox'); + await usersCheckbox.check(); + await expect(usersCheckbox).toBeChecked(); }); - await test.step('Check child fields: id, description, bannerImage', async () => { + await test.step('Check child fields: id, name, email', async () => { const fieldsSection = qb(page).locator('.query-builder-tree'); - const idField = fieldsSection.locator('.field-node').filter({ hasText: /^id\b/ }).first(); + const idField = fieldsSection.locator('.field-node').filter({ hasText: /^id/ }).first(); await idField.locator('.field-checkbox').check(); - const descField = fieldsSection.locator('.field-node').filter({ hasText: /^description/ }).first(); - await descField.locator('.field-checkbox').check(); + const nameField = fieldsSection.locator('.field-node').filter({ hasText: /^name/ }).first(); + await nameField.locator('.field-checkbox').check(); - const bannerField = fieldsSection.locator('.field-node').filter({ hasText: /^bannerImage/ }).first(); - await bannerField.locator('.field-checkbox').check(); + const emailField = fieldsSection.locator('.field-node').filter({ hasText: /^email/ }).first(); + await emailField.locator('.field-checkbox').check(); }); await test.step('Verify query is generated in the editor', async () => { @@ -84,30 +84,25 @@ test.describe('GraphQL Query Builder', () => { await expect.poll(() => getQueryEditorContent(page)).toContain('id'); const editorContent = await getQueryEditorContent(page); expect(editorContent).toContain('id'); - expect(editorContent).toContain('description'); - expect(editorContent).toContain('bannerImage'); + expect(editorContent).toContain('name'); + expect(editorContent).toContain('email'); }); }); test('Enable argument and set value', async ({ pageWithUserData: page }) => { - await test.step('Expand "Character" field to show arguments', async () => { - const characterField = qb(page).locator('.field-node').filter({ hasText: /^Character/ }).first(); - await characterField.click(); + await test.step('Expand "user" field to show arguments', async () => { + const userField = qb(page).locator('.field-node').filter({ hasText: /^user\b/ }).first(); + await userField.click(); await expect(qb(page).locator('.section-header').filter({ hasText: 'ARGUMENTS' }).first()).toBeVisible(); }); - await test.step('Check the "Character" field', async () => { - const characterCheckbox = qb(page) - .locator('.field-node') - .filter({ hasText: /^Character/ }) - .first() - .locator('.field-checkbox'); - await characterCheckbox.check(); + await test.step('Check the "user" field', async () => { + const userCheckbox = qb(page).locator('.field-node').filter({ hasText: /^user\b/ }).first().locator('.field-checkbox'); + await userCheckbox.check(); }); await test.step('Enable the "id" argument and set a value', async () => { - const argRow = qb(page).locator('.arg-row').filter({ has: page.locator('.arg-name', { hasText: /^id$/ }) }).first(); - await expect(argRow).toBeVisible(); + const argRow = qb(page).locator('.arg-row').filter({ hasText: /^id/ }).first(); const argCheckbox = argRow.locator('.field-checkbox'); await argCheckbox.check(); await expect(argCheckbox).toBeChecked(); @@ -117,78 +112,75 @@ test.describe('GraphQL Query Builder', () => { await argInput.fill('123'); }); - await test.step('Check child field "gender"', async () => { - const genderField = qb(page).locator('.field-node').filter({ hasText: /^gender/ }).first(); - await genderField.locator('.field-checkbox').check(); + await test.step('Check child field "name"', async () => { + const nameField = qb(page).locator('.field-node').filter({ hasText: /^name/ }).first(); + await nameField.locator('.field-checkbox').check(); }); await test.step('Verify generated query contains the argument', async () => { await expect.poll(() => getQueryEditorContent(page)).toContain('$id'); const editorContent = await getQueryEditorContent(page); - expect(editorContent).toContain('gender'); + expect(editorContent).toContain('name'); expect(editorContent).toContain('$id'); }); await test.step('Verify variables pane contains the argument value', async () => { const variablesContent = await getVariablesEditorContent(page); expect(variablesContent).toContain('"id"'); - expect(variablesContent).toContain('123'); + expect(variablesContent).toContain('"123"'); }); }); test('Expand nested object types', async ({ pageWithUserData: page }) => { - await test.step('Expand "Staff" field', async () => { - const staffField = qb(page).locator('.field-node').filter({ hasText: /^Staff/ }).first(); - await staffField.click(); + await test.step('Expand "post" field', async () => { + const postField = qb(page).locator('.field-node').filter({ hasText: /^post/ }).first(); + await postField.click(); }); - await test.step('Check "Staff" and expand "name" nested field', async () => { - const staffCheckbox = qb(page).locator('.field-node').filter({ hasText: /^Staff/ }).first().locator('.field-checkbox'); - await staffCheckbox.check(); + await test.step('Check "post" and expand "author" nested field', async () => { + const postCheckbox = qb(page).locator('.field-node').filter({ hasText: /^post/ }).first().locator('.field-checkbox'); + await postCheckbox.check(); - const descField = qb(page).locator('.field-node').filter({ hasText: /^description/ }).first(); - await descField.locator('.field-checkbox').check(); + const titleField = qb(page).locator('.field-node').filter({ hasText: /^title/ }).first(); + await titleField.locator('.field-checkbox').check(); - const nameField = qb(page).locator('.field-node').filter({ hasText: /^name/ }).first(); - await nameField.click(); + const authorField = qb(page).locator('.field-node').filter({ hasText: /^author/ }).first(); + await authorField.click(); }); - await test.step('Select nested name fields', async () => { - const nameCheckbox = qb(page).locator('.field-node').filter({ hasText: /^name/ }).first().locator('.field-checkbox'); - await nameCheckbox.check(); + await test.step('Select nested author fields', async () => { + const authorCheckbox = qb(page).locator('.field-node').filter({ hasText: /^author/ }).first().locator('.field-checkbox'); + await authorCheckbox.check(); - const firstField = qb(page).locator('.field-node').filter({ hasText: /^first/ }).first(); - await firstField.locator('.field-checkbox').check(); + const nameFields = qb(page).locator('.field-node').filter({ hasText: /^name/ }); + const authorNameField = nameFields.nth(1); + await authorNameField.locator('.field-checkbox').check(); }); await test.step('Verify nested query structure in editor', async () => { - await expect.poll(() => getQueryEditorContent(page)).toContain('Staff'); + await expect.poll(() => getQueryEditorContent(page)).toContain('post'); const editorContent = await getQueryEditorContent(page); - expect(editorContent).toContain('description'); + expect(editorContent).toContain('title'); + expect(editorContent).toContain('author'); expect(editorContent).toContain('name'); - expect(editorContent).toContain('first'); }); }); test('Removing a field in code editor unchecks it in query builder', async ({ pageWithUserData: page }) => { - await test.step('Ensure "Media" is expanded with child fields id, description, bannerImage checked', async () => { - const mediaField = qb(page).locator('.field-node').filter({ hasText: /^Media/ }).first(); - const mediaChildrenVisible = await qb(page) - .locator('.field-node') - .filter({ hasText: /^bannerImage/ }) - .first() - .isVisible(); - if (!mediaChildrenVisible) { - await mediaField.click(); + await test.step('Ensure "users" is expanded with child fields id, name, email checked', async () => { + const usersField = qb(page).locator('.field-node').filter({ hasText: /^users/ }).first(); + const usersChildrenVisible = await qb(page).locator('.field-node').filter({ hasText: /^email/ }).first().isVisible(); + if (!usersChildrenVisible) { + await usersField.click(); } - const mediaCheckbox = mediaField.locator('.field-checkbox'); - if (!(await mediaCheckbox.isChecked())) { - await mediaCheckbox.check(); + const usersCheckbox = usersField.locator('.field-checkbox'); + if (!(await usersCheckbox.isChecked())) { + await usersCheckbox.check(); } const fieldsSection = qb(page).locator('.query-builder-tree'); - for (const fieldName of ['id\\b', 'description', 'bannerImage']) { + for (const fieldName of ['id', 'name', 'email']) { const field = fieldsSection.locator('.field-node').filter({ hasText: new RegExp(`^${fieldName}`) }).first(); const checkbox = field.locator('.field-checkbox'); if (!(await checkbox.isChecked())) { @@ -196,16 +188,15 @@ test.describe('GraphQL Query Builder', () => { } } - await expect.poll(() => getQueryEditorContent(page)).toContain('bannerImage'); - // Wait for the Tree→Editor generation debounce (150ms) to complete + await expect.poll(() => getQueryEditorContent(page)).toContain('email'); await page.waitForTimeout(200); }); - await test.step('Remove "bannerImage" field from the code editor', async () => { + await test.step('Remove "email" field from the code editor', async () => { const content = await getQueryEditorContent(page); const updatedContent = content .split('\n') - .filter((line: string) => !line.trim().startsWith('bannerImage')) + .filter((line: string) => !line.trim().startsWith('email')) .join('\n'); // Set content directly via CodeMirror @@ -216,45 +207,44 @@ test.describe('GraphQL Query Builder', () => { }, updatedContent); }); - await test.step('Verify "bannerImage" checkbox is unchecked in query builder', async () => { + await test.step('Verify "email" checkbox is unchecked in query builder', async () => { const fieldsSection = qb(page).locator('.query-builder-tree'); - const bannerCheckbox = fieldsSection + const emailCheckbox = fieldsSection .locator('.field-node') - .filter({ hasText: /^bannerImage/ }) + .filter({ hasText: /^email/ }) .first() .locator('.field-checkbox'); - await expect(bannerCheckbox).not.toBeChecked(); + await expect(emailCheckbox).not.toBeChecked(); }); - await test.step('Verify "id" and "description" are still checked', async () => { + await test.step('Verify "id" and "name" are still checked', async () => { const fieldsSection = qb(page).locator('.query-builder-tree'); - const idCheckbox = fieldsSection.locator('.field-node').filter({ hasText: /^id\b/ }).first().locator('.field-checkbox'); + const idCheckbox = fieldsSection.locator('.field-node').filter({ hasText: /^id/ }).first().locator('.field-checkbox'); await expect(idCheckbox).toBeChecked(); - const descCheckbox = fieldsSection + const nameCheckbox = fieldsSection .locator('.field-node') - .filter({ hasText: /^description/ }) + .filter({ hasText: /^name/ }) .first() .locator('.field-checkbox'); - await expect(descCheckbox).toBeChecked(); + await expect(nameCheckbox).toBeChecked(); }); }); test('Changing variable value in variables editor updates argument in query builder', async ({ pageWithUserData: page }) => { - await test.step('Set up "Character" field with "id" argument via query builder', async () => { - const characterField = qb(page).locator('.field-node').filter({ hasText: /^Character/ }).first(); - await characterField.click(); + await test.step('Set up "user" field with "id" argument via query builder', async () => { + const userField = qb(page).locator('.field-node').filter({ hasText: /^user\b/ }).first(); await expect(qb(page).locator('.section-header').filter({ hasText: 'ARGUMENTS' }).first()).toBeVisible(); - const characterCheckbox = characterField.locator('.field-checkbox'); - if (!(await characterCheckbox.isChecked())) { - await characterCheckbox.check(); + const userCheckbox = userField.locator('.field-checkbox'); + if (!(await userCheckbox.isChecked())) { + await userCheckbox.check(); } - const argRow = qb(page).locator('.arg-row').filter({ has: page.locator('.arg-name', { hasText: /^id$/ }) }).first(); + const argRow = qb(page).locator('.arg-row').filter({ hasText: /^id/ }).first(); const argCheckbox = argRow.locator('.field-checkbox'); if (!(await argCheckbox.isChecked())) { await argCheckbox.check(); @@ -262,19 +252,19 @@ test.describe('GraphQL Query Builder', () => { const argInput = argRow.locator('input[type="text"]'); await argInput.fill('100'); - const genderField = qb(page).locator('.field-node').filter({ hasText: /^gender/ }).first(); - const genderCheckbox = genderField.locator('.field-checkbox'); - if (!(await genderCheckbox.isChecked())) { - await genderCheckbox.check(); + const nameField = qb(page).locator('.field-node').filter({ hasText: /^name/ }).first(); + const nameCheckbox = nameField.locator('.field-checkbox'); + if (!(await nameCheckbox.isChecked())) { + await nameCheckbox.check(); } await expect.poll(() => getQueryEditorContent(page)).toContain('$id'); - await expect.poll(() => getVariablesEditorContent(page)).toContain('100'); + await expect.poll(() => getVariablesEditorContent(page)).toContain('"100"'); }); await test.step('Change the variable value in the variables editor', async () => { const variablesContent = await getVariablesEditorContent(page); - const updatedVariables = variablesContent.replace('100', '999'); + const updatedVariables = variablesContent.replace('"100"', '"999"'); // Set content directly via CodeMirror await ensureVariablesPaneOpen(page); @@ -286,7 +276,7 @@ test.describe('GraphQL Query Builder', () => { }); await test.step('Verify the argument value is updated in query builder', async () => { - const argRow = qb(page).locator('.arg-row').filter({ has: page.locator('.arg-name', { hasText: /^id$/ }) }).first(); + const argRow = qb(page).locator('.arg-row').filter({ hasText: /^id/ }).first(); const argInput = argRow.locator('input[type="text"]'); await expect(argInput).toHaveValue('999'); });