diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 979795d4..4abf2dca 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -105,6 +105,8 @@ jobs: LOGIN_EMAIL: user@automatisch.io LOGIN_PASSWORD: sample BASE_URL: http://localhost:3000 + GITHUB_CLIENT_ID: 1c0417daf898adfbd99a + GITHUB_CLIENT_SECRET: 3328fa814dd582ccd03dbe785cfd683fb8da92b3 run: yarn test - uses: actions/upload-artifact@v3 if: always() diff --git a/packages/e2e-tests/fixtures/applications-modal.js b/packages/e2e-tests/fixtures/applications-modal.js new file mode 100644 index 00000000..3a0d1de8 --- /dev/null +++ b/packages/e2e-tests/fixtures/applications-modal.js @@ -0,0 +1,34 @@ +const { GithubPage } = require('./apps/github/github-page'); +const { BasePage } = require('./base-page'); + +export class ApplicationsModal extends BasePage { + + applications = { + github: GithubPage + }; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor (page) { + super(page); + this.modal = page.getByTestId('add-app-connection-dialog'); + this.searchInput = page.getByTestId('search-for-app-text-field'); + this.appListItem = page.getByTestId('app-list-item'); + this.appLoader = page.getByTestId('search-for-app-loader'); + } + + /** + * @param string link + */ + async selectLink (link) { + if (this.applications[link] === undefined) { + throw { + message: `Unknown link "${link}" passed to ApplicationsModal.selectLink` + } + } + await this.searchInput.fill(link); + await this.appListItem.first().click(); + return new this.applications[link](this.page); + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/applications-page.js b/packages/e2e-tests/fixtures/applications-page.js index f7b6abc4..a84958e6 100644 --- a/packages/e2e-tests/fixtures/applications-page.js +++ b/packages/e2e-tests/fixtures/applications-page.js @@ -1,4 +1,5 @@ const path = require('node:path'); +const { ApplicationsModal } = require('./applications-modal'); const { AuthenticatedPage } = require('./authenticated-page'); export class ApplicationsPage extends AuthenticatedPage { @@ -13,4 +14,9 @@ export class ApplicationsPage extends AuthenticatedPage { this.drawerLink = this.page.getByTestId('apps-page-drawer-link'); this.addConnectionButton = this.page.getByTestId('add-connection-button'); } + + async openAddConnectionModal () { + await this.addConnectionButton.click(); + return new ApplicationsModal(this.page); + } } diff --git a/packages/e2e-tests/fixtures/apps/github/add-github-connection-modal.js b/packages/e2e-tests/fixtures/apps/github/add-github-connection-modal.js new file mode 100644 index 00000000..f5cd6384 --- /dev/null +++ b/packages/e2e-tests/fixtures/apps/github/add-github-connection-modal.js @@ -0,0 +1,49 @@ +import { GithubPopup } from './github-popup'; + +const { BasePage } = require('../../base-page'); + +export class AddGithubConnectionModal extends BasePage { + + /** + * @param {import('@playwright/test').Page} page + */ + constructor (page) { + super(page); + this.modal = page.getByTestId('add-app-connection-dialog'); + this.oauthRedirectInput = page.getByTestId('oAuthRedirectUrl-text'); + this.clientIdInput = page.getByTestId('consumerKey-text'); + this.clientIdSecretInput = page.getByTestId('consumerSecret-text'); + this.submitButton = page.getByTestId('create-connection-button'); + } + + async visible () { + return await this.modal.isVisible(); + } + + async inputForm () { + await connectionModal.clientIdInput.fill( + process.env.GITHUB_CLIENT_ID + ); + await connectionModal.clientIdSecretInput.fill( + process.env.GITHUB_CLIENT_SECRET + ); + } + + /** + * @returns {import('@playwright/test').Page} + */ + async submit () { + const popupPromise = this.page.waitForEvent('popup'); + await this.submitButton.click(); + const popup = await popupPromise; + await popup.bringToFront(); + return popup; + } + + /** + * @param {import('@playwright/test').Page} page + */ + async handlePopup (page) { + return await GithubPopup.handle(page); + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/apps/github/github-page.js b/packages/e2e-tests/fixtures/apps/github/github-page.js new file mode 100644 index 00000000..f1a05d5f --- /dev/null +++ b/packages/e2e-tests/fixtures/apps/github/github-page.js @@ -0,0 +1,65 @@ +const { BasePage } = require('../../base-page'); +const { AddGithubConnectionModal } = require('./add-github-connection-modal'); + +export class GithubPage extends BasePage { + + constructor (page) { + super(page) + this.addConnectionButton = page.getByTestId('add-connection-button'); + this.connectionsTab = page.getByTestId('connections-tab'); + this.flowsTab = page.getByTestId('flows-tab'); + this.connectionRows = page.getByTestId('connection-row'); + this.flowRows = page.getByTestId('flow-row'); + this.firstConnectionButton = page.getByTestId('connections-no-results'); + this.firstFlowButton = page.getByTestId('flows-no-results'); + this.addConnectionModal = new AddGithubConnectionModal(page); + } + + async goto () { + await this.page.goto('/app/github/connections'); + } + + async openConnectionModal () { + await this.addConnectionButton.click(); + await expect(this.addConnectionButton.modal).toBeVisible(); + return this.addConnectionModal; + } + + async flowsVisible () { + return this.page.url() === await this.flowsTab.getAttribute('href'); + } + + async connectionsVisible () { + return this.page.url() === await this.connectionsTab.getAttribute('href'); + } + + async hasFlows () { + if (!(await this.flowsVisible())) { + await this.flowsTab.click(); + await expect(this.flowsTab).toBeVisible(); + } + return await this.flowRows.count() > 0 + } + + async hasConnections () { + if (!(await this.connectionsVisible())) { + await this.connectionsTab.click(); + await expect(this.connectionsTab).toBeVisible(); + } + return await this.connectionRows.count() > 0; + } +} + +/** + * + * @param {import('@playwright/test').Page} page + */ +export async function initGithubConnection (page) { + // assumes already logged in + const githubPage = new GithubPage(page); + await githubPage.goto(); + const modal = await githubPage.openConnectionModal(); + await modal.inputForm(); + const popup = await modal.submit(); + await modal.handlePopup(popup); +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/apps/github/github-popup.js b/packages/e2e-tests/fixtures/apps/github/github-popup.js new file mode 100644 index 00000000..0bdb8579 --- /dev/null +++ b/packages/e2e-tests/fixtures/apps/github/github-popup.js @@ -0,0 +1,92 @@ +const { BasePage } = require('../../base-page'); + +export class GithubPopup extends BasePage { + + /** + * @param {import('@playwright/test').Page} page + */ + static async handle (page) { + const popup = new GithubPopup(page); + return await popup.handleAuthFlow(); + } + + getPathname () { + const url = this.page.url() + try { + return new URL(url).pathname; + } catch (e) { + return new URL(`https://github.com/${url}`).pathname; + } + } + + async handleAuthFlow () { + if (this.getPathname() === '/login') { + await this.handleLogin(); + } + if (this.page.isClosed()) { return; } + if (this.getPathname() === '/login/oauth/authorize') { + await this.handleAuthorize(); + } + } + + async handleLogin () { + const loginInput = this.page.getByLabel('Username or email address'); + loginInput.click(); + await loginInput.fill(process.env.GITHUB_USERNAME); + const passwordInput = this.page.getByLabel('Password'); + passwordInput.click() + await passwordInput.fill(process.env.GITHUB_PASSWORD); + await this.page.getByRole('button', { name: 'Sign in' }).click(); + // await this.page.waitForTimeout(2000); + if (this.page.isClosed()) { + return + } + // await this.page.waitForLoadState('networkidle', 30000); + this.page.waitForEvent('load'); + if (this.page.isClosed()) { + return + } + await this.page.waitForURL(function (url) { + const u = new URL(url); + return ( + u.pathname === '/login/oauth/authorize' + ) && u.searchParams.get('client_id'); + }); + } + + async handleAuthorize () { + if (this.page.isClosed()) { return } + const authorizeButton = this.page.getByRole( + 'button', + { name: 'Authorize' } + ); + await this.page.waitForEvent('load'); + await authorizeButton.click(); + await this.page.waitForURL(function (url) { + const u = new URL(url); + return ( + u.pathname === '/login/oauth/authorize' + ) && ( + u.searchParams.get('client_id') === null + ); + }) + const passwordInput = this.page.getByLabel('Password'); + if (await passwordInput.isVisible()) { + await passwordInput.fill(process.env.GITHUB_PASSWORD); + const submitButton = this.page + .getByRole('button') + .filter({ hasText: /confirm|submit|enter|go|sign in/gmi }); + if (await submitButton.isVisible()) { + submitButton.waitFor(); + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + } else { + throw { + page: this.page, + error: 'Could not find submit button for confirming user account' + }; + } + } + await this.page.waitForEvent('close') + } +} \ No newline at end of file diff --git a/packages/e2e-tests/tests/app-integrations/github.spec.js b/packages/e2e-tests/tests/app-integrations/github.spec.js new file mode 100644 index 00000000..8627f633 --- /dev/null +++ b/packages/e2e-tests/tests/app-integrations/github.spec.js @@ -0,0 +1,63 @@ +const { test, expect } = require('../../fixtures'); + +test('Github OAuth integration', async ({ page, applicationsPage }) => { + const githubConnectionPage = await test.step( + 'Navigate to github connections modal', + async () => { + await applicationsPage.drawerLink.click(); + if (page.url() !== '/apps') { + await page.waitForURL('/apps'); + } + const connectionModal = await applicationsPage.openAddConnectionModal(); + expect(connectionModal.modal).toBeVisible(); + return await connectionModal.selectLink('github'); + } + ); + + const connectionModal = await test.step( + 'Ensure the github connection modal is visible', + async () => { + const connectionModal = githubConnectionPage.addConnectionModal; + expect(connectionModal.modal).toBeVisible(); + return connectionModal; + } + ); + + const githubPopup = await test.step( + 'Input data into the add connection form and submit', + async () => { + await connectionModal.clientIdInput.fill(process.env.GITHUB_CLIENT_ID); + await connectionModal.clientIdSecretInput.fill( + process.env.GITHUB_CLIENT_SECRET + ); + return await connectionModal.submit(); + } + ); + + await test.step('Ensure github popup is not a 404', async () => { + // expect(githubPopup).toBeVisible(); + const title = await githubPopup.title(); + expect(title).not.toMatch(/^Page not found/); + }); + + /* Skip these in CI + await test.step( + 'Handle github popup authentication flow', + async () => { + await connectionModal.handlePopup(githubPopup); + } + ); + + await test.step( + 'Ensure the new connection is added to the connections list', + async () => { + await page.locator('body').click({ position: { x: 0, y: 0 } }); + // TODO + } + ); + */ +}); + +test.afterAll(async () => { + // TODO - Remove connections from github connections page +}); \ No newline at end of file diff --git a/packages/web/src/components/AppConnections/index.tsx b/packages/web/src/components/AppConnections/index.tsx index 35dea8bc..46c99afd 100644 --- a/packages/web/src/components/AppConnections/index.tsx +++ b/packages/web/src/components/AppConnections/index.tsx @@ -29,6 +29,7 @@ export default function AppConnections( ); } diff --git a/packages/web/src/components/AppFlows/index.tsx b/packages/web/src/components/AppFlows/index.tsx index 6e7e5ee0..4e40c5fd 100644 --- a/packages/web/src/components/AppFlows/index.tsx +++ b/packages/web/src/components/AppFlows/index.tsx @@ -45,6 +45,7 @@ export default function AppFlows(props: AppFlowsProps): React.ReactElement { ); } diff --git a/packages/web/src/components/InputCreator/index.tsx b/packages/web/src/components/InputCreator/index.tsx index 0d701db4..52638c2d 100644 --- a/packages/web/src/components/InputCreator/index.tsx +++ b/packages/web/src/components/InputCreator/index.tsx @@ -190,6 +190,7 @@ export default function InputCreator( onChange={onChange} onBlur={onBlur} name={computedName} + data-test={`${computedName}-text`} label={label} fullWidth helperText={description} diff --git a/packages/web/src/pages/Application/index.tsx b/packages/web/src/pages/Application/index.tsx index f76a3dab..8db364c6 100644 --- a/packages/web/src/pages/Application/index.tsx +++ b/packages/web/src/pages/Application/index.tsx @@ -175,6 +175,7 @@ export default function Application(): React.ReactElement | null { value={URLS.APP_CONNECTIONS_PATTERN} disabled={!app.supportsConnections} component={Link} + data-test="connections-tab" />