From 3b0d52c104a7e5b6656b64ee19cbdb92dc08711b Mon Sep 17 00:00:00 2001 From: Zacharias Fragkiadakis Date: Sun, 1 Dec 2024 14:47:00 +0200 Subject: [PATCH 1/4] test(e2e): write tests for more onboarding screens Signed-off-by: Zacharias Fragkiadakis --- test/e2e/specs/onboarding.spec.ts | 75 ++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/test/e2e/specs/onboarding.spec.ts b/test/e2e/specs/onboarding.spec.ts index 071599ee..da7f7fbf 100644 --- a/test/e2e/specs/onboarding.spec.ts +++ b/test/e2e/specs/onboarding.spec.ts @@ -1,7 +1,7 @@ import path from 'node:path' import { browser, expect } from '@wdio/globals' import type { ViewSection, Workbench } from 'wdio-vscode-service' -import { $, cd } from 'zx' +import { $, cd, echo } from 'zx' import type * as VsCode from 'vscode' import { e2eTestDirPath } from '../constants/config' @@ -9,11 +9,10 @@ describe('Onboarding Flow', () => { let workbench: Workbench before(async () => { - await initGitRepo() workbench = await browser.getWorkbench() }) - describe('VS Code, *before* the workspace is rad-initialized,', () => { + describe('VS Code, *before* Radicle is installed,', () => { it('has our Radicle extension installed and available', async () => { const extensions = await browser.executeWorkbench( (vscode: typeof VsCode) => vscode.extensions.all, @@ -31,6 +30,55 @@ describe('Onboarding Flow', () => { expect(title).toBe('Radicle') }) + it('instructs the user to install radicle', async () => { + await openRadicleViewContainer(workbench) + + const welcomeText = await getFirstWelcomeViewText(workbench) + const buttonTitles = await getFirstWelcomeViewButtonTitles(workbench) + + expect(welcomeText).toEqual([ + /* eslint-disable max-len */ + 'Failed resolving the Radicle CLI binary.', + "Please ensure it is installed on your machine and either that it is globally accessible in the shell as `rad` or that its path is correctly defined in the extension's settings.", + "Please expect the extention's capabilities to remain severely limited until this issue is resolved.", + /* eslint-enable max-len */ + ]) + + expect(buttonTitles).toEqual(['Troubleshoot']) + }) + }) + + describe('VS Code, *before* the workspace is git-initialized,', () => { + before(() => { + installRadicle() + }) + + it('guides the user on how to git-initialize their workspace', async () => { + await openRadicleViewContainer(workbench) + + const welcomeText = await getFirstWelcomeViewText(workbench) + const welcomeButtonTitles = await getFirstWelcomeViewButtonTitles(workbench) + + expect(welcomeText).toEqual([ + /* eslint-disable max-len */ + 'The folder currently opened in your workspace is not a Git code repository.', + 'In order to use Radicle with it, this folder must first be initialized as a Git code repository.', + 'To learn more about how to use Git and source control in VS Code read the docs.', + /* eslint-enable max-len */ + ]) + + expect(welcomeButtonTitles).toEqual([ + 'Initialize Repository With Git', + 'Choose a Different Folder', + ]) + }) + }) + + describe('VS Code, *before* the workspace is rad-initialized,', () => { + before(async () => { + await initGitRepo() + }) + it('guides the user on how to rad-initialize their git repo', async () => { await openRadicleViewContainer(workbench) @@ -45,8 +93,6 @@ describe('Onboarding Flow', () => { 'To learn more read the Radicle User Guide.', /* eslint-enable max-len */ ]) - - expect(welcomeText.some((text) => text.includes('rad init'))).toBe(true) }) }) @@ -97,6 +143,11 @@ describe('Onboarding Flow', () => { }) }) +function installRadicle() { + // TODO: zac implement this + echo('To be implemented') +} + async function initGitRepo() { const repoDirPath = path.join(e2eTestDirPath, 'fixtures/workspaces/basic') @@ -132,3 +183,17 @@ async function getFirstWelcomeViewText(workbench: Workbench) { return welcomeText } + +async function getFirstWelcomeViewButtonTitles(workbench: Workbench) { + const sidebarView = workbench.getSideBar().getContent() + await sidebarView.wait() + + const buttons = + (await (await (await sidebarView.getSections())[0]?.findWelcomeContent())?.getButtons()) ?? + [] + const buttonTitles = await Promise.all( + buttons.map(async (button) => await button.getTitle()), + ) + + return buttonTitles +} From 481d6266d839f9e29628d6f9133e0260d2b571d4 Mon Sep 17 00:00:00 2001 From: Zacharias Fragkiadakis Date: Thu, 26 Dec 2024 16:47:16 +0200 Subject: [PATCH 2/4] test(e2e): make some assertions asynchronous - Refactors some tests in onboarding.spec.ts using `browser.waitUntil` to reduce flakiness - These assertions will now poll for the result instead of failing immediately - Simulates Radicle installation in the onboarding process by renaming the node home directory - Extracts common queries, actions, and assertions to helper files Signed-off-by: Zacharias Fragkiadakis --- test/e2e/constants/config.ts | 6 ++ test/e2e/helpers/actions.ts | 15 +++ test/e2e/helpers/assertions.ts | 24 +++++ test/e2e/helpers/queries.ts | 21 +++++ test/e2e/specs/onboarding.spec.ts | 152 ++++++++++++------------------ 5 files changed, 125 insertions(+), 93 deletions(-) create mode 100644 test/e2e/helpers/actions.ts create mode 100644 test/e2e/helpers/assertions.ts create mode 100644 test/e2e/helpers/queries.ts diff --git a/test/e2e/constants/config.ts b/test/e2e/constants/config.ts index c1e31607..5effe2c3 100644 --- a/test/e2e/constants/config.ts +++ b/test/e2e/constants/config.ts @@ -11,3 +11,9 @@ export const e2eTestDirPath = join(__dirname, '..') * files outside of the e2e test directory. */ export const rootDirPath = join(__dirname, '../../..') + +/** + * The path to the node home directory. This is where the Radicle node stores its + * configuration and data. + */ +export const nodeHomePath = process.env['RAD_HOME'] diff --git a/test/e2e/helpers/actions.ts b/test/e2e/helpers/actions.ts new file mode 100644 index 00000000..b9fabbcf --- /dev/null +++ b/test/e2e/helpers/actions.ts @@ -0,0 +1,15 @@ +import type { Workbench } from 'wdio-vscode-service' + +/** + * Opens the Radicle view container in the sidebar, by clicking the radicle button in the + * activity bar. + */ +export async function openRadicleViewContainer(workbench: Workbench) { + const activityBar = workbench.getActivityBar() + await activityBar.wait() + + const radicleViewControl = await activityBar.getViewControl('Radicle') + await radicleViewControl?.wait() + + await radicleViewControl?.openView() +} diff --git a/test/e2e/helpers/assertions.ts b/test/e2e/helpers/assertions.ts new file mode 100644 index 00000000..a0ed5ba7 --- /dev/null +++ b/test/e2e/helpers/assertions.ts @@ -0,0 +1,24 @@ +import { browser } from '@wdio/globals' +import type { Workbench } from 'wdio-vscode-service' + +/** + * Asserts that the CLI Commands and Patches sections are visible in the sidebar. This is + * considered the default state when the workspace is open with a git and rad initialized + * repository. + */ +export async function expectCliCommandsAndPatchesToBeVisible(workbench: Workbench) { + const sidebarView = workbench.getSideBar().getContent() + await sidebarView.wait() + + await browser.waitUntil(async () => { + let sectionsFound = false + try { + await sidebarView.getSection('CLI COMMANDS') + await sidebarView.getSection('PATCHES') + sectionsFound = true + // eslint-disable-next-line prettier-vue/prettier + } catch { } + + return sectionsFound + }) +} diff --git a/test/e2e/helpers/queries.ts b/test/e2e/helpers/queries.ts new file mode 100644 index 00000000..1efa6d57 --- /dev/null +++ b/test/e2e/helpers/queries.ts @@ -0,0 +1,21 @@ +import type { Workbench } from 'wdio-vscode-service' + +/** + * Retrieves the welcome text content from the first section in the sidebar view. + * + * @example await getFirstWelcomeViewText(workbench) // ["Welcome to Radicle!", "Get started by ..."] + * + * @returns The text content found as an array of strings split by newlines. If no content is + * found, an empty array. + */ +export async function getFirstWelcomeViewText(workbench: Workbench) { + const sidebarView = workbench.getSideBar().getContent() + await sidebarView.wait() + + const welcomeText = + (await ( + await (await sidebarView.getSections())[0]?.findWelcomeContent() + )?.getTextSections()) ?? [] + + return welcomeText +} diff --git a/test/e2e/specs/onboarding.spec.ts b/test/e2e/specs/onboarding.spec.ts index da7f7fbf..f2fb7dc0 100644 --- a/test/e2e/specs/onboarding.spec.ts +++ b/test/e2e/specs/onboarding.spec.ts @@ -1,9 +1,13 @@ import path from 'node:path' import { browser, expect } from '@wdio/globals' -import type { ViewSection, Workbench } from 'wdio-vscode-service' -import { $, cd, echo } from 'zx' +import type { Workbench } from 'wdio-vscode-service' +import { $, cd } from 'zx' import type * as VsCode from 'vscode' -import { e2eTestDirPath } from '../constants/config' +import isEqual from 'lodash/isEqual' +import { expectCliCommandsAndPatchesToBeVisible } from '../helpers/assertions' +import { openRadicleViewContainer } from '../helpers/actions' +import { getFirstWelcomeViewText } from '../helpers/queries' +import { e2eTestDirPath, nodeHomePath } from '../constants/config' describe('Onboarding Flow', () => { let workbench: Workbench @@ -13,6 +17,18 @@ describe('Onboarding Flow', () => { }) describe('VS Code, *before* Radicle is installed,', () => { + const tempPathToNodeHome = `${nodeHomePath}.temp` + + before(async () => { + // Simulate radicle "not being installed" by renaming the node home directory + await $`mv ${nodeHomePath} ${tempPathToNodeHome}` + }) + + after(async () => { + await $`mv ${tempPathToNodeHome} ${nodeHomePath}` + await workbench.executeCommand('Developer: Reload Window') + }) + it('has our Radicle extension installed and available', async () => { const extensions = await browser.executeWorkbench( (vscode: typeof VsCode) => vscode.extensions.all, @@ -49,28 +65,27 @@ describe('Onboarding Flow', () => { }) describe('VS Code, *before* the workspace is git-initialized,', () => { - before(() => { - installRadicle() - }) - it('guides the user on how to git-initialize their workspace', async () => { await openRadicleViewContainer(workbench) - const welcomeText = await getFirstWelcomeViewText(workbench) - const welcomeButtonTitles = await getFirstWelcomeViewButtonTitles(workbench) - - expect(welcomeText).toEqual([ - /* eslint-disable max-len */ - 'The folder currently opened in your workspace is not a Git code repository.', - 'In order to use Radicle with it, this folder must first be initialized as a Git code repository.', - 'To learn more about how to use Git and source control in VS Code read the docs.', - /* eslint-enable max-len */ - ]) - - expect(welcomeButtonTitles).toEqual([ - 'Initialize Repository With Git', - 'Choose a Different Folder', - ]) + await browser.waitUntil(async () => { + const welcomeText = await getFirstWelcomeViewText(workbench) + const welcomeButtonTitles = await getFirstWelcomeViewButtonTitles(workbench) + + return ( + isEqual(welcomeText, [ + /* eslint-disable max-len */ + 'The folder currently opened in your workspace is not a Git code repository.', + 'In order to use Radicle with it, this folder must first be initialized as a Git code repository.', + 'To learn more about how to use Git and source control in VS Code read the docs.', + /* eslint-enable max-len */ + ]) && + isEqual(welcomeButtonTitles, [ + 'Initialize Repository With Git', + 'Choose a Different Folder', + ]) + ) + }) }) }) @@ -82,72 +97,45 @@ describe('Onboarding Flow', () => { it('guides the user on how to rad-initialize their git repo', async () => { await openRadicleViewContainer(workbench) - const welcomeText = await getFirstWelcomeViewText(workbench) - - expect(welcomeText).toEqual([ - /* eslint-disable max-len */ - 'The Git repository currently opened in your workspace is not yet initialized with Radicle.', - 'To use Radicle with it, please run `rad init` in your terminal.', - 'Once rad-initialized, this repo will have access to advanced source control, collaboration and project management capabilities powered by both Git and Radicle.', - 'During this reversible rad-initializing process you also get to choose whether your repo will be private or public, among other options.', - 'To learn more read the Radicle User Guide.', - /* eslint-enable max-len */ - ]) + await browser.waitUntil(async () => { + const welcomeText = await getFirstWelcomeViewText(workbench) + + return isEqual(welcomeText, [ + /* eslint-disable max-len */ + 'The Git repository currently opened in your workspace is not yet initialized with Radicle.', + 'To use Radicle with it, please run `rad init` in your terminal.', + 'Once rad-initialized, this repo will have access to advanced source control, collaboration and project management capabilities powered by both Git and Radicle.', + 'During this reversible rad-initializing process you also get to choose whether your repo will be private or public, among other options.', + 'To learn more read the Radicle User Guide.', + /* eslint-enable max-len */ + ]) + }) }) }) describe('VS Code, *after* the workspace is rad-initialized,', () => { - let cliCommandsSection: ViewSection - before(async () => { await $`rad init --private --default-branch main --name "A_test_blog" --description "Some repo" --no-confirm --verbose` - await openRadicleViewContainer(workbench) - const sidebarView = workbench.getSideBar().getContent() - await sidebarView.wait() - - cliCommandsSection = await sidebarView.getSection('CLI COMMANDS') - await cliCommandsSection.collapse() - - const patchesSection = await sidebarView.getSection('PATCHES') - await patchesSection.collapse() + await workbench.executeCommand('Developer: Reload Window') }) it('hides the non rad-initialized guide', async () => { - const welcomeText = await getFirstWelcomeViewText(workbench) + await browser.waitUntil(async () => { + const welcomeText = await getFirstWelcomeViewText(workbench) - expect(welcomeText.some((text) => text.includes('rad init'))).not.toBe(true) + return welcomeText.some((text) => text.includes('rad init')) === false + }) }) - it('shows the CLI Commands section', async () => { - await cliCommandsSection.expand() - // Fixes flakiness on macOS CI - await browser.pause(100) - - const welcomeContent = await cliCommandsSection?.findWelcomeContent() - const welcomeText = (await welcomeContent?.getTextSections()) ?? [] - const buttons = (await welcomeContent?.getButtons()) ?? [] - const buttonTitles = await Promise.all( - buttons.map(async (button) => await button.getTitle()), - ) - - expect( - welcomeText.some((text) => - /Use the buttons below to perform common interactions with the Radicle network./i.test( - text, - ), - ), - ).toBe(true) + it('shows the CLI Commands and Patches sections', async () => { + const sidebarView = workbench.getSideBar().getContent() + await sidebarView.wait() - expect(buttonTitles).toEqual(['Sync', 'Fetch', 'Announce']) + await expectCliCommandsAndPatchesToBeVisible(workbench) }) }) }) -function installRadicle() { - // TODO: zac implement this - echo('To be implemented') -} - async function initGitRepo() { const repoDirPath = path.join(e2eTestDirPath, 'fixtures/workspaces/basic') @@ -162,28 +150,6 @@ async function initGitRepo() { await $`git commit -m 'adds readme' --no-gpg-sign` } -async function openRadicleViewContainer(workbench: Workbench) { - const activityBar = workbench.getActivityBar() - await activityBar.wait() - - const radicleViewControl = await activityBar.getViewControl('Radicle') - await radicleViewControl?.wait() - - await radicleViewControl?.openView() -} - -async function getFirstWelcomeViewText(workbench: Workbench) { - const sidebarView = workbench.getSideBar().getContent() - await sidebarView.wait() - - const welcomeText = - (await ( - await (await sidebarView.getSections())[0]?.findWelcomeContent() - )?.getTextSections()) ?? [] - - return welcomeText -} - async function getFirstWelcomeViewButtonTitles(workbench: Workbench) { const sidebarView = workbench.getSideBar().getContent() await sidebarView.wait() From adf2a6d7db98fea445b2c9f88a7d1b7b65cfc064 Mon Sep 17 00:00:00 2001 From: Zacharias Fragkiadakis Date: Tue, 31 Dec 2024 10:14:42 +0200 Subject: [PATCH 3/4] refactor(e2e): rename and enhance standard sidebar views assertion Signed-off-by: Zacharias Fragkiadakis --- test/e2e/helpers/assertions.ts | 26 +++++++++++++++----------- test/e2e/specs/onboarding.spec.ts | 6 +++--- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/test/e2e/helpers/assertions.ts b/test/e2e/helpers/assertions.ts index a0ed5ba7..539d5c31 100644 --- a/test/e2e/helpers/assertions.ts +++ b/test/e2e/helpers/assertions.ts @@ -6,19 +6,23 @@ import type { Workbench } from 'wdio-vscode-service' * considered the default state when the workspace is open with a git and rad initialized * repository. */ -export async function expectCliCommandsAndPatchesToBeVisible(workbench: Workbench) { +export async function expectStandardSidebarViewsToBeVisible(workbench: Workbench) { const sidebarView = workbench.getSideBar().getContent() await sidebarView.wait() - await browser.waitUntil(async () => { - let sectionsFound = false - try { - await sidebarView.getSection('CLI COMMANDS') - await sidebarView.getSection('PATCHES') - sectionsFound = true - // eslint-disable-next-line prettier-vue/prettier - } catch { } + await browser.waitUntil( + async () => { + try { + await Promise.all([ + sidebarView.getSection('CLI COMMANDS'), + sidebarView.getSection('PATCHES'), + ]) - return sectionsFound - }) + return true + } catch { + return false + } + }, + { timeoutMsg: 'expected the standard sidebar views to be visible' }, + ) } diff --git a/test/e2e/specs/onboarding.spec.ts b/test/e2e/specs/onboarding.spec.ts index f2fb7dc0..5cd0bb4f 100644 --- a/test/e2e/specs/onboarding.spec.ts +++ b/test/e2e/specs/onboarding.spec.ts @@ -4,7 +4,7 @@ import type { Workbench } from 'wdio-vscode-service' import { $, cd } from 'zx' import type * as VsCode from 'vscode' import isEqual from 'lodash/isEqual' -import { expectCliCommandsAndPatchesToBeVisible } from '../helpers/assertions' +import { expectStandardSidebarViewsToBeVisible } from '../helpers/assertions' import { openRadicleViewContainer } from '../helpers/actions' import { getFirstWelcomeViewText } from '../helpers/queries' import { e2eTestDirPath, nodeHomePath } from '../constants/config' @@ -127,11 +127,11 @@ describe('Onboarding Flow', () => { }) }) - it('shows the CLI Commands and Patches sections', async () => { + it('shows the standard sidebar views', async () => { const sidebarView = workbench.getSideBar().getContent() await sidebarView.wait() - await expectCliCommandsAndPatchesToBeVisible(workbench) + await expectStandardSidebarViewsToBeVisible(workbench) }) }) }) From cf748660d03e6e418a467a3eebbc8893efb92330 Mon Sep 17 00:00:00 2001 From: Zacharias Fragkiadakis Date: Tue, 7 Jan 2025 20:37:57 +0200 Subject: [PATCH 4/4] test(e2e): move radicle directory **before** launching vscode - Uses wdio's `onWorkerStart` hook to move the radicle directory before launching vscode - This is only done on the onboarding suite worker Signed-off-by: Zacharias Fragkiadakis --- test/e2e/constants/config.ts | 7 +++++++ test/e2e/specs/onboarding.spec.ts | 11 ++--------- test/e2e/wdio.conf.ts | 12 +++++++++++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/test/e2e/constants/config.ts b/test/e2e/constants/config.ts index 5effe2c3..1a5e6f66 100644 --- a/test/e2e/constants/config.ts +++ b/test/e2e/constants/config.ts @@ -17,3 +17,10 @@ export const rootDirPath = join(__dirname, '../../..') * configuration and data. */ export const nodeHomePath = process.env['RAD_HOME'] + +/** + * The path to a backup node home directory. This is used to store the original + * node home directory for tests that require the original path to be moved or + * modified. + */ +export const backupNodeHomePath = `${nodeHomePath}.backup` diff --git a/test/e2e/specs/onboarding.spec.ts b/test/e2e/specs/onboarding.spec.ts index 5cd0bb4f..fff8b4e2 100644 --- a/test/e2e/specs/onboarding.spec.ts +++ b/test/e2e/specs/onboarding.spec.ts @@ -7,7 +7,7 @@ import isEqual from 'lodash/isEqual' import { expectStandardSidebarViewsToBeVisible } from '../helpers/assertions' import { openRadicleViewContainer } from '../helpers/actions' import { getFirstWelcomeViewText } from '../helpers/queries' -import { e2eTestDirPath, nodeHomePath } from '../constants/config' +import { backupNodeHomePath, e2eTestDirPath, nodeHomePath } from '../constants/config' describe('Onboarding Flow', () => { let workbench: Workbench @@ -17,15 +17,8 @@ describe('Onboarding Flow', () => { }) describe('VS Code, *before* Radicle is installed,', () => { - const tempPathToNodeHome = `${nodeHomePath}.temp` - - before(async () => { - // Simulate radicle "not being installed" by renaming the node home directory - await $`mv ${nodeHomePath} ${tempPathToNodeHome}` - }) - after(async () => { - await $`mv ${tempPathToNodeHome} ${nodeHomePath}` + await $`mv ${backupNodeHomePath} ${nodeHomePath}` await workbench.executeCommand('Developer: Reload Window') }) diff --git a/test/e2e/wdio.conf.ts b/test/e2e/wdio.conf.ts index 1844457d..d95a3414 100644 --- a/test/e2e/wdio.conf.ts +++ b/test/e2e/wdio.conf.ts @@ -1,7 +1,12 @@ import path from 'node:path' import { $ } from 'zx' import type { Options } from '@wdio/types' -import { e2eTestDirPath, rootDirPath } from './constants/config' +import { + backupNodeHomePath, + e2eTestDirPath, + nodeHomePath, + rootDirPath, +} from './constants/config' // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const packageJson = require('../../package.json') @@ -56,4 +61,9 @@ export const config: Options.Testrunner = { onPrepare: async () => { await $`mkdir -p ${path.join(rootDirPath, 'node_modules/.cache/wdio')}` }, + onWorkerStart: async (_cid, _caps, specs) => { + if (specs.some((spec) => spec.includes('onboarding.spec.ts'))) { + await $`mv ${nodeHomePath} ${backupNodeHomePath}` + } + }, }