diff --git a/.vscode-test.mjs b/.vscode-test.mjs index 586b08d4e5..c3406cbc00 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -8,7 +8,7 @@ function generateConfig(label) { /** @type {import('@vscode/test-cli').TestConfiguration} */ let config = { label, - files: ["out/**/*.test.js"], + files: ["dist/**/*.test.js"], version: "insiders", srcDir: "src", launchArgs: [ diff --git a/.vscodeignore b/.vscodeignore index 906a576e03..3c9c0e53af 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -12,6 +12,7 @@ node_modules/** scripts/** src/** webviews/** +**/test/** .eslintcache* .eslintignore .eslintrc* diff --git a/src/test/browser/index.ts b/src/test/browser/index.ts index 4a34fbef75..ead80a2baf 100644 --- a/src/test/browser/index.ts +++ b/src/test/browser/index.ts @@ -1,3 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-nocheck // This file is providing the test runner to use when running extension tests. import * as vscode from 'vscode'; require('mocha/mocha'); diff --git a/src/test/builders/graphql/latestReviewCommitBuilder.ts b/src/test/builders/graphql/latestReviewCommitBuilder.ts index a51807ae01..07ab147fbd 100644 --- a/src/test/builders/graphql/latestReviewCommitBuilder.ts +++ b/src/test/builders/graphql/latestReviewCommitBuilder.ts @@ -8,7 +8,7 @@ import { LatestReviewCommitResponse } from '../../../github/graphql'; import { RateLimitBuilder } from './rateLimitBuilder'; -type Repository = LatestReviewCommitResponse['repository']; +type Repository = NonNullable; type PullRequest = Repository['pullRequest']; type ViewerLatestReview = PullRequest['viewerLatestReview']; type Commit = ViewerLatestReview['commit']; diff --git a/src/test/builders/graphql/pullRequestBuilder.ts b/src/test/builders/graphql/pullRequestBuilder.ts index afd5942cb1..cd8b73682e 100644 --- a/src/test/builders/graphql/pullRequestBuilder.ts +++ b/src/test/builders/graphql/pullRequestBuilder.ts @@ -37,7 +37,7 @@ const RefBuilder = createBuilderClass()({ }), }); -type Repository = PullRequestResponse['repository']; +type Repository = NonNullable; type PullRequest = Repository['pullRequest']; type Author = PullRequest['author']; type AssigneesConn = PullRequest['assignees']; diff --git a/src/test/builders/graphql/timelineEventsBuilder.ts b/src/test/builders/graphql/timelineEventsBuilder.ts index 48eeb16e79..ec8a12b955 100644 --- a/src/test/builders/graphql/timelineEventsBuilder.ts +++ b/src/test/builders/graphql/timelineEventsBuilder.ts @@ -1,9 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + import { createBuilderClass, createLink } from '../base'; import { TimelineEventsResponse } from '../../../github/graphql'; import { RateLimitBuilder } from './rateLimitBuilder'; -type Repository = TimelineEventsResponse['repository']; +type Repository = NonNullable; type PullRequest = Repository['pullRequest']; type TimelineConn = PullRequest['timelineItems']; diff --git a/src/test/builders/rest/pullRequestBuilder.ts b/src/test/builders/rest/pullRequestBuilder.ts index 70af54d5f8..d65f476c16 100644 --- a/src/test/builders/rest/pullRequestBuilder.ts +++ b/src/test/builders/rest/pullRequestBuilder.ts @@ -1,5 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + import { UserBuilder } from './userBuilder'; -import { RefBuilder } from './refBuilder'; +import { NonNullUserRefBuilder, RefBuilder } from './refBuilder'; import { createLink, createBuilderClass } from '../base'; import { OctokitCommon } from '../../../github/common'; @@ -40,8 +45,8 @@ export const PullRequestBuilder = createBuilderClass()({ closed_at: { default: '' }, merged_at: { default: '' }, merge_commit_sha: { default: '' }, - head: { linked: RefBuilder }, - base: { linked: RefBuilder }, + head: { linked: NonNullUserRefBuilder }, + base: { linked: NonNullUserRefBuilder }, draft: { default: false }, merged: { default: false }, mergeable: { default: true }, diff --git a/src/test/builders/rest/refBuilder.ts b/src/test/builders/rest/refBuilder.ts index 6796e9a19f..8e4f070616 100644 --- a/src/test/builders/rest/refBuilder.ts +++ b/src/test/builders/rest/refBuilder.ts @@ -1,3 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + import { UserBuilder } from './userBuilder'; import { RepositoryBuilder } from './repoBuilder'; import { createBuilderClass } from '../base'; @@ -5,7 +10,7 @@ import { OctokitCommon } from '../../../github/common'; type RefUnion = OctokitCommon.PullsListResponseItemHead & OctokitCommon.PullsListResponseItemBase; -export const RefBuilder = createBuilderClass()({ +export const RefBuilder = createBuilderClass>()({ label: { default: 'octocat:new-feature' }, ref: { default: 'new-feature' }, user: { linked: UserBuilder }, @@ -14,4 +19,15 @@ export const RefBuilder = createBuilderClass()({ repo: { linked: RepositoryBuilder }, }); +// Variant where user is guaranteed non-null. +type NonNullUserRef = Omit & { user: NonNullable }; + +export const NonNullUserRefBuilder = createBuilderClass()({ + label: { default: 'octocat:new-feature' }, + ref: { default: 'new-feature' }, + user: { linked: UserBuilder }, // non-null guarantee + sha: { default: '0000000000000000000000000000000000000000' }, + repo: { linked: RepositoryBuilder }, +}); + export type RefBuilder = InstanceType; diff --git a/src/test/builders/rest/repoBuilder.ts b/src/test/builders/rest/repoBuilder.ts index 2fc8038731..3f05aaf494 100644 --- a/src/test/builders/rest/repoBuilder.ts +++ b/src/test/builders/rest/repoBuilder.ts @@ -16,7 +16,7 @@ type License = RepoUnion['license']; type Permissions = RepoUnion['permissions']; type CodeOfConduct = RepoUnion['code_of_conduct']; -export const RepositoryBuilder = createBuilderClass()({ +export const RepositoryBuilder = createBuilderClass>()({ id: { default: 0 }, node_id: { default: 'node0' }, name: { default: 'reponame' }, @@ -123,9 +123,9 @@ export const RepositoryBuilder = createBuilderClass()({ name: { default: 'name' }, url: { default: 'https://github.com/octocat/reponame' }, }), - forks: { default: null }, - open_issues: { default: null }, - watchers: { default: null }, + forks: { default: 0 }, + open_issues: { default: 0 }, + watchers: { default: 0 }, }); export type RepositoryBuilder = InstanceType; diff --git a/src/test/builders/rest/userBuilder.ts b/src/test/builders/rest/userBuilder.ts index c1846eab71..412b21548d 100644 --- a/src/test/builders/rest/userBuilder.ts +++ b/src/test/builders/rest/userBuilder.ts @@ -17,7 +17,9 @@ type UserUnion = | OctokitCommon.PullsListResponseItemHeadRepoOwner | OctokitCommon.IssuesListEventsForTimelineResponseItemActor; -export const UserBuilder = createBuilderClass>()({ +type NonNullUser = NonNullable; + +export const UserBuilder = createBuilderClass()({ id: { default: 0 }, node_id: { default: 'node0' }, login: { default: 'octocat' }, diff --git a/src/test/github/markdownUtils.test.ts b/src/test/github/markdownUtils.test.ts index 99b8801603..3f0be975f1 100644 --- a/src/test/github/markdownUtils.test.ts +++ b/src/test/github/markdownUtils.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert'; import * as marked from 'marked'; import { PlainTextRenderer } from '../../github/markdownUtils'; -import { describe, it } from 'mocha'; describe('PlainTextRenderer', () => { it('should escape inline code by default', () => { diff --git a/src/test/index.ts b/src/test/index.ts index 677bc56144..05ef9f9859 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,3 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-nocheck // This file is providing the test runner to use when running extension tests. import * as path from 'path'; import * as vscode from 'vscode'; @@ -6,20 +12,6 @@ import Mocha from 'mocha'; import { mockWebviewEnvironment } from './mocks/mockWebviewEnvironment'; import { EXTENSION_ID } from '../constants'; -function addTests(mocha: Mocha, root: string): Promise { - return new Promise((resolve, reject) => { - glob('**/**.test.js', { cwd: root }, (error, files) => { - if (error) { - return reject(error); - } - - for (const testFile of files) { - mocha.addFile(path.join(root, testFile)); - } - resolve(); - }); - }); -} async function runAllExtensionTests(testsRoot: string, clb: (error: Error | null, failures?: number) => void): Promise { // Ensure the dev-mode extension is activated @@ -31,10 +23,22 @@ async function runAllExtensionTests(testsRoot: string, clb: (error: Error | null ui: 'bdd', color: true }); - mocha.addFile(path.resolve(testsRoot, 'globalHooks.js')); + // Load globalHooks if it exists + try { + mocha.addFile(path.resolve(testsRoot, 'globalHooks.js')); + } catch (e) { + // globalHooks might not exist in webpack bundle, ignore + } - await addTests(mocha, testsRoot); - await addTests(mocha, path.resolve(testsRoot, '../../../webviews/')); + // Import all test files using webpack's require.context + try { + // Load tests from src/test directory only + // Webview tests are compiled separately with the webview configuration + const importAll = (r: __WebpackModuleApi.RequireContext) => r.keys().forEach(r); + importAll(require.context('./', true, /\.test$/)); + } catch (e) { + console.log('Error loading tests:', e); + } if (process.env.TEST_JUNIT_XML_PATH) { mocha.reporter('mocha-multi-reporters', { diff --git a/src/test/mocks/mockGitHubRepository.ts b/src/test/mocks/mockGitHubRepository.ts index 08f0cad6a6..4bff09d7e9 100644 --- a/src/test/mocks/mockGitHubRepository.ts +++ b/src/test/mocks/mockGitHubRepository.ts @@ -21,8 +21,7 @@ import { MockTelemetry } from './mockTelemetry'; import { Uri } from 'vscode'; import { LoggingOctokit, RateLogger } from '../../github/loggingOctokit'; import { mergeQuerySchemaWithShared } from '../../github/common'; -import { PullRequestModel } from '../../github/pullRequestModel'; -import { TimelineEvent } from '../../common/timelineEvent'; + const queries = mergeQuerySchemaWithShared(require('../../github/queries.gql'), require('../../github/queriesShared.gql')) as any; export class MockGitHubRepository extends GitHubRepository { @@ -35,7 +34,7 @@ export class MockGitHubRepository extends GitHubRepository { this._hub = { octokit: new LoggingOctokit(this.queryProvider.octokit, new RateLogger(new MockTelemetry(), true)), - graphql: null, + graphql: {} as any, }; this._metadata = Promise.resolve({ @@ -73,8 +72,8 @@ export class MockGitHubRepository extends GitHubRepository { block(builder); const responses = builder.build(); - const prNumber = responses.pullRequest.repository.pullRequest.number; - const headRef = responses.pullRequest.repository.pullRequest.headRef; + const prNumber = responses.pullRequest.repository!.pullRequest.number; + const headRef = responses.pullRequest.repository?.pullRequest.headRef; this.queryProvider.expectGraphQLQuery( { diff --git a/src/test/view/prsTree.test.ts b/src/test/view/prsTree.test.ts index a3d9c339c6..1f72287dd1 100644 --- a/src/test/view/prsTree.test.ts +++ b/src/test/view/prsTree.test.ts @@ -74,7 +74,7 @@ describe('GitHub Pull Requests view', function () { userAgent: 'GitHub VSCode Pull Requests', previews: ['shadow-cat-preview'], }), new RateLogger(telemetry, true)), - graphql: null, + graphql: {} as any, }; return github; @@ -160,7 +160,7 @@ describe('GitHub Pull Requests view', function () { ); }); }).pullRequest; - const prItem0 = await parseGraphQLPullRequest(pr0.repository.pullRequest, gitHubRepository); + const prItem0 = await parseGraphQLPullRequest(pr0.repository!.pullRequest, gitHubRepository); const pullRequest0 = new PullRequestModel(credentialStore, telemetry, gitHubRepository, remote, prItem0); const pr1 = gitHubRepository.addGraphQLPullRequest(builder => { @@ -177,7 +177,7 @@ describe('GitHub Pull Requests view', function () { ); }); }).pullRequest; - const prItem1 = await parseGraphQLPullRequest(pr1.repository.pullRequest, gitHubRepository); + const prItem1 = await parseGraphQLPullRequest(pr1.repository!.pullRequest, gitHubRepository); const pullRequest1 = new PullRequestModel(credentialStore, telemetry, gitHubRepository, remote, prItem1); const repository = new MockRepository(); diff --git a/tsconfig.json b/tsconfig.json index b60d27e54d..c84f7118b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,6 @@ }, "exclude": [ "node_modules", - "src/test", "webviews" ] } \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 17f46cfe12..250d4933f8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -74,6 +74,9 @@ async function getWebviewConfig(mode, env, entry) { output: { filename: '[name].js', path: path.resolve(__dirname, 'dist'), + // Use absolute paths (file:///) in source maps instead of the default webpack:// scheme + devtoolModuleFilenameTemplate: info => 'file:///' + info.absoluteResourcePath.replace(/\\/g, '/'), + devtoolFallbackModuleFilenameTemplate: 'file:///[absolute-resource-path]' }, optimization: { minimizer: [ @@ -156,14 +159,12 @@ async function getWebviewConfig(mode, env, entry) { */ async function getExtensionConfig(target, mode, env) { const basePath = path.join(__dirname, 'src'); + const glob = require('glob'); /** * @type WebpackConfig['plugins'] | any */ const plugins = [ - new webpack.optimize.LimitChunkCountPlugin({ - maxChunks: 1 - }), new ForkTsCheckerPlugin({ async: false, formatter: 'basic', @@ -174,6 +175,43 @@ async function getExtensionConfig(target, mode, env) { new webpack.ContextReplacementPlugin(/mocha/, /^$/) ]; + // Add fixtures copying plugin for node target (which has individual test files) + if (target === 'node') { + const fs = require('fs'); + const srcRoot = 'src'; + class CopyFixturesPlugin { + apply(compiler) { + compiler.hooks.afterEmit.tap('CopyFixturesPlugin', () => { + this.copyFixtures(srcRoot, compiler.options.output.path); + }); + } + + copyFixtures(inputDir, outputDir) { + try { + const files = fs.readdirSync(inputDir); + for (const file of files) { + const filePath = path.join(inputDir, file); + const stats = fs.statSync(filePath); + if (stats.isDirectory()) { + if (file === 'fixtures') { + const outputFilePath = path.join(outputDir, inputDir.substring(srcRoot.length), file); + const inputFilePath = path.join(inputDir, file); + fs.cpSync(inputFilePath, outputFilePath, { recursive: true, force: true }); + } else { + this.copyFixtures(filePath, outputDir); + } + } + } + } catch (error) { + // Ignore errors during fixtures copying to not break the build + console.warn('Warning: Could not copy fixtures:', error.message); + } + } + } + + plugins.push(new CopyFixturesPlugin()); + } + if (target === 'webworker') { plugins.push(new webpack.ProvidePlugin({ process: path.join( @@ -190,8 +228,28 @@ async function getExtensionConfig(target, mode, env) { const entry = { extension: './src/extension.ts', }; + + // Add test entry points if (target === 'webworker') { entry['test/index'] = './src/test/browser/index.ts'; + } else if (target === 'node') { + // Add main test runner + entry['test/index'] = './src/test/index.ts'; + + // Add individual test files as separate entry points + const testFiles = glob.sync('src/test/**/*.test.ts', { cwd: __dirname }); + testFiles.forEach(testFile => { + // Convert src/test/github/utils.test.ts -> test/github/utils.test + const entryName = testFile.replace('src/', '').replace('.ts', ''); + entry[entryName] = `./${testFile}`; + }); + } + + // Don't limit chunks for node target when we have individual test files + if (target !== 'node' || !('test/index' in entry && Object.keys(entry).some(key => key.endsWith('.test')))) { + plugins.unshift(new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1 + })); } return { @@ -205,6 +263,9 @@ async function getExtensionConfig(target, mode, env) { libraryTarget: 'commonjs2', filename: '[name].js', chunkFilename: 'feature-[name].js', + // Use absolute paths (file:///) in source maps for easier debugging of tests & sources + devtoolModuleFilenameTemplate: info => 'file:///' + info.absoluteResourcePath.replace(/\\/g, '/'), + devtoolFallbackModuleFilenameTemplate: 'file:///[absolute-resource-path]', }, optimization: { minimizer: [