addons/isl/src/__tests__/ComparisonView.test.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {RenderResult} from '@testing-library/react';
b69ab319
b69ab3110import {act, cleanup, fireEvent, render, screen, waitFor, within} from '@testing-library/react';
b69ab3111import fs from 'node:fs';
b69ab3112import path from 'node:path';
b69ab3113import {ComparisonType} from 'shared/Comparison';
b69ab3114import {nextTick} from 'shared/testUtils';
b69ab3115import {nullthrows} from 'shared/utils';
b69ab3116import App from '../App';
b69ab3117import {cancelAllHighlightingTasks} from '../ComparisonView/SplitDiffView/syntaxHighlighting';
b69ab3118import {parsePatchAndFilter, sortFilesByType} from '../ComparisonView/utils';
b69ab3119import {
b69ab3120 COMMIT,
b69ab3121 expectMessageSentToServer,
b69ab3122 openCommitInfoSidebar,
b69ab3123 resetTestMessages,
b69ab3124 simulateCommits,
b69ab3125 simulateMessageFromServer,
b69ab3126 simulateUncommittedChangedFiles,
b69ab3127 waitForWithTick,
b69ab3128} from '../testUtils';
b69ab3129import {GeneratedStatus} from '../types';
b69ab3130
b69ab3131afterEach(cleanup);
b69ab3132
b69ab3133const UNCOMMITTED_CHANGES_DIFF = `\
b69ab3134diff --git deletedFile.txt deletedFile.txt
b69ab3135deleted file mode 100644
b69ab3136--- deletedFile.txt
b69ab3137+++ /dev/null
b69ab3138@@ -1,1 +0,0 @@
b69ab3139-Goodbye
b69ab3140diff --git newFile.txt newFile.txt
b69ab3141new file mode 100644
b69ab3142--- /dev/null
b69ab3143+++ newFile.txt
b69ab3144@@ -0,0 +1,1 @@
b69ab3145+hello
b69ab3146diff --git someFile.txt someFile.txt
b69ab3147--- someFile.txt
b69ab3148+++ someFile.txt
b69ab3149@@ -7,5 +7,5 @@
b69ab3150 line 7
b69ab3151 line 8
b69ab3152-line 9
b69ab3153+line 9 - modified
b69ab3154 line 10
b69ab3155 line 11
b69ab3156diff --git -r a1b2c3d4e5f6 some/path/foo.go
b69ab3157--- some/path/foo.go
b69ab3158+++ some/path/foo.go
b69ab3159@@ -0,1 +0,1 @@
b69ab3160-println("hi")
b69ab3161+fmt.Println("hi")
b69ab3162`;
b69ab3163
b69ab3164const DIFF_WITH_SYNTAX = `\
b69ab3165diff --git deletedFile.js deletedFile.js
b69ab3166deleted file mode 100644
b69ab3167--- deletedFile.js
b69ab3168+++ /dev/null
b69ab3169@@ -1,1 +0,0 @@
b69ab3170-console.log('goodbye');
b69ab3171diff --git newFile.js newFile.js
b69ab3172new file mode 100644
b69ab3173--- /dev/null
b69ab3174+++ newFile.js
b69ab3175@@ -0,0 +1,1 @@
b69ab3176+console.log('hello');
b69ab3177diff --git someFile.js someFile.js
b69ab3178--- someFile.js
b69ab3179+++ someFile.js
b69ab3180@@ -2,5 +2,5 @@
b69ab3181 function foo() {
b69ab3182 const variable_in_context_line = 0;
b69ab3183- const variable_in_before = 1;
b69ab3184+ const variable_in_after = 1;
b69ab3185 console.log(variable_in_content_line);
b69ab3186 }
b69ab3187`;
b69ab3188
b69ab3189Object.defineProperty(navigator, 'clipboard', {
b69ab3190 value: {
b69ab3191 writeText: jest.fn(() => Promise.resolve()),
b69ab3192 },
b69ab3193});
b69ab3194
b69ab3195/* eslint-disable @typescript-eslint/no-non-null-assertion */
b69ab3196
b69ab3197describe('ComparisonView', () => {
b69ab3198 let app: RenderResult | null = null;
b69ab3199 beforeEach(() => {
b69ab31100 mockFetchToSupportSyntaxHighlighting();
b69ab31101 resetTestMessages();
b69ab31102 app = render(<App />);
b69ab31103 act(() => {
b69ab31104 openCommitInfoSidebar();
b69ab31105 simulateCommits({
b69ab31106 value: [
b69ab31107 COMMIT('1', 'some public base', '0', {phase: 'public'}),
b69ab31108 COMMIT('a', 'My Commit', '1'),
b69ab31109 COMMIT('b', 'Another Commit', 'a', {isDot: true}),
b69ab31110 ],
b69ab31111 });
b69ab31112 simulateUncommittedChangedFiles({
b69ab31113 value: [{path: 'src/file1.txt', status: 'M'}],
b69ab31114 });
b69ab31115 });
b69ab31116 });
b69ab31117
b69ab31118 // Not afterEach because afterEach runs in a separate tick and can be too
b69ab31119 // late to avoid act(..) warnings or timeout.
b69ab31120 function unmountNow() {
b69ab31121 cancelAllHighlightingTasks();
b69ab31122 app?.unmount();
b69ab31123 }
b69ab31124
b69ab31125 afterEach(() => {
b69ab31126 jest.clearAllMocks();
b69ab31127 });
b69ab31128
b69ab31129 async function clickComparisonViewButton() {
b69ab31130 await act(async () => {
b69ab31131 const button = screen.getByTestId('open-comparison-view-button-Uncommitted');
b69ab31132 fireEvent.click(button);
b69ab31133 await nextTick();
b69ab31134 });
b69ab31135 }
b69ab31136 async function openUncommittedChangesComparison(
b69ab31137 diffContent?: string,
b69ab31138 generatedStatuses?: Record<string, GeneratedStatus>,
b69ab31139 ) {
b69ab31140 await clickComparisonViewButton();
b69ab31141 await waitFor(
b69ab31142 () =>
b69ab31143 expectMessageSentToServer({
b69ab31144 type: 'requestComparison',
b69ab31145 comparison: {type: ComparisonType.UncommittedChanges},
b69ab31146 }),
b69ab31147 // Since this dynamically imports the comparison view, it may take a while to load in resource-constrained CI,
b69ab31148 // so add a generous timeout to reducy flakiness.
b69ab31149 {timeout: 10_000},
b69ab31150 );
b69ab31151 act(() => {
b69ab31152 simulateMessageFromServer({
b69ab31153 type: 'fetchedGeneratedStatuses',
b69ab31154 results: generatedStatuses ?? {},
b69ab31155 });
b69ab31156 });
b69ab31157 await act(async () => {
b69ab31158 simulateMessageFromServer({
b69ab31159 type: 'comparison',
b69ab31160 comparison: {type: ComparisonType.UncommittedChanges},
b69ab31161 data: {diff: {value: diffContent ?? UNCOMMITTED_CHANGES_DIFF}},
b69ab31162 });
b69ab31163 await nextTick();
b69ab31164 });
b69ab31165 }
b69ab31166 function inComparisonView() {
b69ab31167 return within(screen.getByTestId('comparison-view'));
b69ab31168 }
b69ab31169
b69ab31170 function closeComparisonView() {
b69ab31171 const closeButton = inComparisonView().getByTestId('close-comparison-view-button');
b69ab31172 expect(closeButton).toBeInTheDocument();
b69ab31173 act(() => {
b69ab31174 fireEvent.click(closeButton);
b69ab31175 });
b69ab31176 }
b69ab31177
b69ab31178 it('Loads comparison', async () => {
b69ab31179 await openUncommittedChangesComparison();
b69ab31180 // Prevent act(..) warnings. This cannot be afterEach() which is too late.
b69ab31181 unmountNow();
b69ab31182 });
b69ab31183
b69ab31184 it('parses files from comparison', async () => {
b69ab31185 await openUncommittedChangesComparison();
b69ab31186 expect(inComparisonView().getByText('someFile.txt')).toBeInTheDocument();
b69ab31187 expect(inComparisonView().getByText('newFile.txt')).toBeInTheDocument();
b69ab31188 expect(inComparisonView().getByText('deletedFile.txt')).toBeInTheDocument();
b69ab31189 unmountNow();
b69ab31190 });
b69ab31191
b69ab31192 it('show file contents', async () => {
b69ab31193 await openUncommittedChangesComparison();
b69ab31194 expect(inComparisonView().getByText('- modified')).toBeInTheDocument();
b69ab31195 expect(inComparisonView().getAllByText('line 7')[0]).toBeInTheDocument();
b69ab31196 expect(inComparisonView().getAllByText('line 8')[0]).toBeInTheDocument();
b69ab31197 expect(inComparisonView().getAllByText('line 9')[0]).toBeInTheDocument();
b69ab31198 expect(inComparisonView().getAllByText('line 10')[0]).toBeInTheDocument();
b69ab31199 expect(inComparisonView().getAllByText('line 11')[0]).toBeInTheDocument();
b69ab31200 unmountNow();
b69ab31201 });
b69ab31202
b69ab31203 it('loads remaining lines', async () => {
b69ab31204 await openUncommittedChangesComparison();
b69ab31205 const expandButton = inComparisonView().getByText('Expand 6 lines');
b69ab31206 expect(expandButton).toBeInTheDocument();
b69ab31207 act(() => {
b69ab31208 fireEvent.click(expandButton);
b69ab31209 });
b69ab31210 await waitFor(() => {
b69ab31211 expectMessageSentToServer({
b69ab31212 type: 'requestComparisonContextLines',
b69ab31213 id: {path: 'someFile.txt', comparison: {type: ComparisonType.UncommittedChanges}},
b69ab31214 numLines: 6,
b69ab31215 start: 1,
b69ab31216 });
b69ab31217 });
b69ab31218 act(() => {
b69ab31219 simulateMessageFromServer({
b69ab31220 type: 'comparisonContextLines',
b69ab31221 lines: {value: ['line 1', 'line 2', 'line 3', 'line 4', 'line 5', 'line 6']},
b69ab31222 path: 'someFile.txt',
b69ab31223 });
b69ab31224 });
b69ab31225 await waitFor(() => {
b69ab31226 expect(inComparisonView().getAllByText('line 1')[0]).toBeInTheDocument();
b69ab31227 expect(inComparisonView().getAllByText('line 2')[0]).toBeInTheDocument();
b69ab31228 expect(inComparisonView().getAllByText('line 3')[0]).toBeInTheDocument();
b69ab31229 expect(inComparisonView().getAllByText('line 4')[0]).toBeInTheDocument();
b69ab31230 expect(inComparisonView().getAllByText('line 5')[0]).toBeInTheDocument();
b69ab31231 expect(inComparisonView().getAllByText('line 6')[0]).toBeInTheDocument();
b69ab31232 });
b69ab31233 unmountNow();
b69ab31234 });
b69ab31235
b69ab31236 it('can close comparison', async () => {
b69ab31237 await openUncommittedChangesComparison();
b69ab31238 expect(inComparisonView().getByText('- modified')).toBeInTheDocument();
b69ab31239 closeComparisonView();
b69ab31240 expect(screen.queryByText('- modified')).not.toBeInTheDocument();
b69ab31241 unmountNow();
b69ab31242 });
b69ab31243
b69ab31244 it('invalidates cached remaining lines when the head commit changes', async () => {
b69ab31245 await openUncommittedChangesComparison();
b69ab31246 const clickExpand = () => {
b69ab31247 const expandButton = inComparisonView().getByText('Expand 6 lines');
b69ab31248 expect(expandButton).toBeInTheDocument();
b69ab31249 act(() => {
b69ab31250 fireEvent.click(expandButton);
b69ab31251 });
b69ab31252 };
b69ab31253 clickExpand();
b69ab31254 await waitFor(() => {
b69ab31255 expectMessageSentToServer({
b69ab31256 type: 'requestComparisonContextLines',
b69ab31257 id: {path: 'someFile.txt', comparison: {type: ComparisonType.UncommittedChanges}},
b69ab31258 numLines: 6,
b69ab31259 start: 1,
b69ab31260 });
b69ab31261 });
b69ab31262 act(() => {
b69ab31263 simulateMessageFromServer({
b69ab31264 type: 'comparisonContextLines',
b69ab31265 lines: {value: ['line 1', 'line 2', 'line 3', 'line 4', 'line 5', 'line 6']},
b69ab31266 path: 'someFile.txt',
b69ab31267 });
b69ab31268 });
b69ab31269 await waitForWithTick(() => {
b69ab31270 expect(inComparisonView().getAllByText('line 1')[0]).toBeInTheDocument();
b69ab31271 expect(inComparisonView().getAllByText('line 6')[0]).toBeInTheDocument();
b69ab31272 });
b69ab31273
b69ab31274 closeComparisonView();
b69ab31275 resetTestMessages(); // make sure we don't find previous "requestComparisonContextLines" in later assertions
b69ab31276
b69ab31277 // head commit changes
b69ab31278
b69ab31279 act(() => {
b69ab31280 simulateCommits({
b69ab31281 value: [
b69ab31282 COMMIT('1', 'some public base', '0', {phase: 'public'}),
b69ab31283 COMMIT('a', 'My Commit', '1'),
b69ab31284 COMMIT('b', 'Another Commit', 'a'),
b69ab31285 COMMIT('c', 'New commit!', 'b', {isDot: true}),
b69ab31286 ],
b69ab31287 });
b69ab31288 });
b69ab31289 await openUncommittedChangesComparison();
b69ab31290 expect(inComparisonView().getByText('- modified')).toBeInTheDocument();
b69ab31291
b69ab31292 clickExpand();
b69ab31293
b69ab31294 // previous context lines are no longer there
b69ab31295 expect(inComparisonView().queryByText('line 1')).not.toBeInTheDocument();
b69ab31296
b69ab31297 // it should ask for the line contents from the server again
b69ab31298 await waitFor(() => {
b69ab31299 expectMessageSentToServer({
b69ab31300 type: 'requestComparisonContextLines',
b69ab31301 id: {path: 'someFile.txt', comparison: {type: ComparisonType.UncommittedChanges}},
b69ab31302 numLines: 6,
b69ab31303 start: 1,
b69ab31304 });
b69ab31305 });
b69ab31306 act(() => {
b69ab31307 simulateMessageFromServer({
b69ab31308 type: 'comparisonContextLines',
b69ab31309 lines: {
b69ab31310 value: [
b69ab31311 'different line 1',
b69ab31312 'different line 2',
b69ab31313 'different line 3',
b69ab31314 'different line 4',
b69ab31315 'different line 5',
b69ab31316 'different line 6',
b69ab31317 ],
b69ab31318 },
b69ab31319 path: 'someFile.txt',
b69ab31320 });
b69ab31321 });
b69ab31322 // new data is used
b69ab31323 await waitForWithTick(() => {
b69ab31324 expect(inComparisonView().getAllByText('different line 1')[0]).toBeInTheDocument();
b69ab31325 expect(inComparisonView().getAllByText('different line 6')[0]).toBeInTheDocument();
b69ab31326 });
b69ab31327 unmountNow();
b69ab31328 });
b69ab31329
b69ab31330 it('refresh button requests new data', async () => {
b69ab31331 await openUncommittedChangesComparison();
b69ab31332 resetTestMessages();
b69ab31333
b69ab31334 act(() => {
b69ab31335 fireEvent.click(inComparisonView().getByTestId('comparison-refresh-button'));
b69ab31336 });
b69ab31337
b69ab31338 expectMessageSentToServer({
b69ab31339 type: 'requestComparison',
b69ab31340 comparison: {type: ComparisonType.UncommittedChanges},
b69ab31341 });
b69ab31342 unmountNow();
b69ab31343 });
b69ab31344
b69ab31345 it('changing comparison mode requests new data', async () => {
b69ab31346 await openUncommittedChangesComparison();
b69ab31347
b69ab31348 act(() => {
b69ab31349 fireEvent.change(inComparisonView().getByTestId('comparison-view-picker'), {
b69ab31350 target: {value: ComparisonType.StackChanges},
b69ab31351 });
b69ab31352 });
b69ab31353 expectMessageSentToServer({
b69ab31354 type: 'requestComparison',
b69ab31355 comparison: {type: ComparisonType.StackChanges},
b69ab31356 });
b69ab31357 unmountNow();
b69ab31358 });
b69ab31359
b69ab31360 it('shows a spinner while a fetch is ongoing', async () => {
b69ab31361 await clickComparisonViewButton();
b69ab31362 expect(inComparisonView().getByTestId('comparison-loading')).toBeInTheDocument();
b69ab31363
b69ab31364 await act(async () => {
b69ab31365 simulateMessageFromServer({
b69ab31366 type: 'comparison',
b69ab31367 comparison: {type: ComparisonType.UncommittedChanges},
b69ab31368 data: {diff: {value: UNCOMMITTED_CHANGES_DIFF}},
b69ab31369 });
b69ab31370 await nextTick();
b69ab31371 });
b69ab31372 expect(inComparisonView().queryByTestId('comparison-loading')).not.toBeInTheDocument();
b69ab31373 unmountNow();
b69ab31374 });
b69ab31375
b69ab31376 it('copies file path on click', async () => {
b69ab31377 await openUncommittedChangesComparison();
b69ab31378
b69ab31379 // Click on the "foo.go" of "some/path/foo.go".
b69ab31380 act(() => {
b69ab31381 fireEvent.click(inComparisonView().getByText('foo.go'));
b69ab31382 });
b69ab31383 expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
b69ab31384 expect(navigator.clipboard.writeText).toHaveBeenCalledWith('foo.go');
b69ab31385
b69ab31386 // Click on the "some/" of "some/path/foo.go".
b69ab31387 act(() => {
b69ab31388 fireEvent.click(inComparisonView().getByText('some/'));
b69ab31389 });
b69ab31390 expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(2);
b69ab31391 expect(navigator.clipboard.writeText).toHaveBeenLastCalledWith('some/path/foo.go');
b69ab31392 unmountNow();
b69ab31393 });
b69ab31394
b69ab31395 describe('syntax highlighting', () => {
b69ab31396 it('renders syntax highlighting', async () => {
b69ab31397 await openUncommittedChangesComparison(DIFF_WITH_SYNTAX);
b69ab31398 await waitForSyntaxHighlightingToAppear(screen.getByTestId('comparison-view'));
b69ab31399
b69ab31400 // console from console.log is highlighted as its own token
b69ab31401 const tokens = within(screen.getByTestId('comparison-view')).queryAllByText('console');
b69ab31402 expect(tokens.length).toBeGreaterThan(0);
b69ab31403 // highlighted tokens have classes like mtk1, mtk2, etc.
b69ab31404 expect(tokens.some(token => /mtk\d+/.test(token.className))).toBe(true);
b69ab31405 unmountNow();
b69ab31406 });
b69ab31407
b69ab31408 it('renders highlighting in context lines and diff lines', async () => {
b69ab31409 await openUncommittedChangesComparison(DIFF_WITH_SYNTAX);
b69ab31410 await waitForSyntaxHighlightingToAppear(screen.getByTestId('comparison-view'));
b69ab31411
b69ab31412 await waitFor(() => {
b69ab31413 expect(
b69ab31414 within(screen.getByTestId('comparison-view')).queryAllByText('variable_in_context_line'),
b69ab31415 ).toHaveLength(2);
b69ab31416 expect(
b69ab31417 within(screen.getByTestId('comparison-view')).getByText('variable_in_before'),
b69ab31418 ).toBeInTheDocument();
b69ab31419 expect(
b69ab31420 within(screen.getByTestId('comparison-view')).getByText('variable_in_after'),
b69ab31421 ).toBeInTheDocument();
b69ab31422 });
b69ab31423 unmountNow();
b69ab31424 });
b69ab31425
b69ab31426 it('highlights expanded context lines', async () => {
b69ab31427 await openUncommittedChangesComparison(DIFF_WITH_SYNTAX);
b69ab31428 await waitForSyntaxHighlightingToAppear(screen.getByTestId('comparison-view'));
b69ab31429
b69ab31430 const expandButton = inComparisonView().getByText('Expand 1 line');
b69ab31431 expect(expandButton).toBeInTheDocument();
b69ab31432 act(() => {
b69ab31433 fireEvent.click(expandButton);
b69ab31434 });
b69ab31435 await waitFor(() => {
b69ab31436 expectMessageSentToServer({
b69ab31437 type: 'requestComparisonContextLines',
b69ab31438 id: {path: 'someFile.js', comparison: {type: ComparisonType.UncommittedChanges}},
b69ab31439 numLines: 1,
b69ab31440 start: 1,
b69ab31441 });
b69ab31442 });
b69ab31443 act(() => {
b69ab31444 simulateMessageFromServer({
b69ab31445 type: 'comparisonContextLines',
b69ab31446 lines: {value: ['const loaded_additional_context_variable = 5;']},
b69ab31447 path: 'someFile.js',
b69ab31448 });
b69ab31449 });
b69ab31450 await waitFor(() => {
b69ab31451 // highlighted token appears by itself
b69ab31452 expect(
b69ab31453 inComparisonView().queryAllByText('loaded_additional_context_variable'),
b69ab31454 ).toHaveLength(2);
b69ab31455 });
b69ab31456 unmountNow();
b69ab31457 });
b69ab31458 });
b69ab31459
b69ab31460 const makeFileDiff = (name: string, content: string) => {
b69ab31461 return `diff --git file${name}.txt file${name}.txt
b69ab31462--- file${name}.txt
b69ab31463+++ file${name}.txt
b69ab31464@@ -1,2 +1,2 @@
b69ab31465${content}
b69ab31466`;
b69ab31467 };
b69ab31468
b69ab31469 describe('collapsing files', () => {
b69ab31470 it('can click to collapse files', async () => {
b69ab31471 const SINGLE_CHANGE = makeFileDiff('1', '+const x = 1;');
b69ab31472 await openUncommittedChangesComparison(SINGLE_CHANGE);
b69ab31473
b69ab31474 const collapseButton = screen.getByTestId('split-diff-view-file-header-collapse-button');
b69ab31475 expect(inComparisonView().getByText('const x = 1;')).toBeInTheDocument();
b69ab31476 expect(inComparisonView().getByText('file1.txt')).toBeInTheDocument();
b69ab31477 fireEvent.click(collapseButton);
b69ab31478 expect(inComparisonView().queryByText('const x = 1;')).not.toBeInTheDocument();
b69ab31479 expect(inComparisonView().getByText('file1.txt')).toBeInTheDocument();
b69ab31480 });
b69ab31481
b69ab31482 it('first files are expanded, later files are collapsed', async () => {
b69ab31483 // 10 files, 1000 added lines each
b69ab31484 const HUGE_DIFF = [
b69ab31485 ...new Array(10)
b69ab31486 .fill(undefined)
b69ab31487 .map((_, index) =>
b69ab31488 makeFileDiff(String(index), new Array(1001).fill("+console.log('hi');").join('\n')),
b69ab31489 ),
b69ab31490 ].join('\n');
b69ab31491 await openUncommittedChangesComparison(HUGE_DIFF);
b69ab31492
b69ab31493 const collapsedStates = inComparisonView().queryAllByTestId(
b69ab31494 /split-diff-view-file-header-(collapse|expand)-button/,
b69ab31495 );
b69ab31496 const collapsedValues = collapsedStates.map(node => node.dataset.testid);
b69ab31497 expect(collapsedValues).toEqual([
b69ab31498 'split-diff-view-file-header-collapse-button',
b69ab31499 'split-diff-view-file-header-collapse-button',
b69ab31500 'split-diff-view-file-header-collapse-button',
b69ab31501 'split-diff-view-file-header-expand-button',
b69ab31502 'split-diff-view-file-header-expand-button',
b69ab31503 'split-diff-view-file-header-expand-button',
b69ab31504 'split-diff-view-file-header-expand-button',
b69ab31505 'split-diff-view-file-header-expand-button',
b69ab31506 'split-diff-view-file-header-expand-button',
b69ab31507 'split-diff-view-file-header-expand-button',
b69ab31508 ]);
b69ab31509 unmountNow();
b69ab31510 }, 20_000 /* potentially slow test */);
b69ab31511
b69ab31512 it('a single large file is expanded so you always see something', async () => {
b69ab31513 const GIANT_FILE = makeFileDiff(
b69ab31514 'bigChange.txt',
b69ab31515 new Array(5000).fill('+big_file_contents').join('\n'),
b69ab31516 );
b69ab31517 const SMALL_FILE = makeFileDiff('smallChange.txt', '+small_file_contents');
b69ab31518 const GIANT_AND_SMALL = [GIANT_FILE, SMALL_FILE].join('\n');
b69ab31519 await openUncommittedChangesComparison(GIANT_AND_SMALL);
b69ab31520
b69ab31521 // the large file starts expanded
b69ab31522 expect(inComparisonView().getAllByText('big_file_contents').length).toBeGreaterThan(0);
b69ab31523 // the small file starts collapsed
b69ab31524 expect(inComparisonView().queryByText('small_file_contents')).not.toBeInTheDocument();
b69ab31525 unmountNow();
b69ab31526 });
b69ab31527 });
b69ab31528
b69ab31529 describe('generated files', () => {
b69ab31530 it('generated status is fetched for files being compared', async () => {
b69ab31531 const NORMAL_FILE = makeFileDiff('normal1', '+normal_contents');
b69ab31532 const PARTIAL_FILE = makeFileDiff('partial1', '+partial_contents');
b69ab31533 const GENERATED_FILE = makeFileDiff('generated1', '+generated_contents');
b69ab31534 const ALL = [GENERATED_FILE, PARTIAL_FILE, NORMAL_FILE].join('\n');
b69ab31535
b69ab31536 await openUncommittedChangesComparison(ALL);
b69ab31537 await waitFor(() => {
b69ab31538 expectMessageSentToServer({
b69ab31539 type: 'fetchGeneratedStatuses',
b69ab31540 paths: ['filegenerated1.txt', 'filepartial1.txt', 'filenormal1.txt'],
b69ab31541 });
b69ab31542 });
b69ab31543 unmountNow();
b69ab31544 });
b69ab31545
b69ab31546 it('banner says that files are generated', async () => {
b69ab31547 const NORMAL_FILE = makeFileDiff('normal2', '+normal_contents');
b69ab31548 const PARTIAL_FILE = makeFileDiff('partial2', '+partial_contents');
b69ab31549 const GENERATED_FILE = makeFileDiff('generated2', '+generated_contents');
b69ab31550 const ALL = [GENERATED_FILE, PARTIAL_FILE, NORMAL_FILE].join('\n');
b69ab31551
b69ab31552 await openUncommittedChangesComparison(ALL, {
b69ab31553 'filenormal2.txt': GeneratedStatus.Manual,
b69ab31554 'filegenerated2.txt': GeneratedStatus.Generated,
b69ab31555 'filepartial2.txt': GeneratedStatus.PartiallyGenerated,
b69ab31556 });
b69ab31557 expect(inComparisonView().getByText('This file is generated')).toBeInTheDocument();
b69ab31558 expect(inComparisonView().getByText('This file is partially generated')).toBeInTheDocument();
b69ab31559 unmountNow();
b69ab31560 });
b69ab31561
b69ab31562 it('generated files are collapsed by default', async () => {
b69ab31563 const NORMAL_FILE = makeFileDiff('normal3', '+normal_contents');
b69ab31564 const PARTIAL_FILE = makeFileDiff('partial3', '+partial_contents');
b69ab31565 const GENERATED_FILE = makeFileDiff('generated3', '+generated_contents');
b69ab31566 const ALL = [GENERATED_FILE, PARTIAL_FILE, NORMAL_FILE].join('\n');
b69ab31567
b69ab31568 await openUncommittedChangesComparison(ALL, {
b69ab31569 'filenormal3.txt': GeneratedStatus.Manual,
b69ab31570 'filegenerated3.txt': GeneratedStatus.Generated,
b69ab31571 'filepartial3.txt': GeneratedStatus.PartiallyGenerated,
b69ab31572 });
b69ab31573
b69ab31574 // normal, partial start expanded
b69ab31575 expect(inComparisonView().getByText('normal_contents')).toBeInTheDocument();
b69ab31576 expect(inComparisonView().getByText('partial_contents')).toBeInTheDocument();
b69ab31577 await waitFor(() => {
b69ab31578 // generated starts collapsed
b69ab31579 expect(inComparisonView().queryByText('generated_contents')).not.toBeInTheDocument();
b69ab31580 });
b69ab31581
b69ab31582 expect(inComparisonView().getByText('Show anyway')).toBeInTheDocument();
b69ab31583 fireEvent.click(inComparisonView().getByText('Show anyway'));
b69ab31584 await waitFor(() => {
b69ab31585 // generated now expands
b69ab31586 expect(inComparisonView().getByText('generated_contents')).toBeInTheDocument();
b69ab31587 });
b69ab31588 unmountNow();
b69ab31589 });
b69ab31590 });
b69ab31591});
b69ab31592
b69ab31593function waitForSyntaxHighlightingToAppear(inside: HTMLElement): Promise<void> {
b69ab31594 return waitFor(() => {
b69ab31595 const tokens = inside.querySelectorAll('.mtk1');
b69ab31596 expect(tokens.length).toBeGreaterThan(0);
b69ab31597 });
b69ab31598}
b69ab31599
b69ab31600function mockFetchToSupportSyntaxHighlighting(): jest.SpyInstance {
b69ab31601 return jest.spyOn(global, 'fetch').mockImplementation(
b69ab31602 jest.fn(async (url: URL) => {
b69ab31603 if (url.pathname.includes('generated/textmate')) {
b69ab31604 const match = /.*generated\/textmate\/(.*)$/.exec(url.pathname);
b69ab31605 const filename = nullthrows(match)[1];
b69ab31606 const toPublicDir = (filename: string) =>
b69ab31607 path.normalize(path.join(__dirname, '../../public/generated/textmate', filename));
b69ab31608 if (filename === 'onig.wasm') {
b69ab31609 const file = await fs.promises.readFile(toPublicDir(filename));
b69ab31610 return {
b69ab31611 headers: new Map(),
b69ab31612 arrayBuffer: () => file.buffer,
b69ab31613 };
b69ab31614 } else {
b69ab31615 const file = await fs.promises.readFile(toPublicDir(filename), 'utf-8');
b69ab31616 return {text: () => file};
b69ab31617 }
b69ab31618 }
b69ab31619 throw new Error(`${url} not found`);
b69ab31620 }) as jest.Mock,
b69ab31621 );
b69ab31622}
b69ab31623
b69ab31624describe('ComparisonView utils', () => {
b69ab31625 describe('sortFilesByType', () => {
b69ab31626 it('sorts by type', () => {
b69ab31627 const files = parsePatchAndFilter(UNCOMMITTED_CHANGES_DIFF);
b69ab31628
b69ab31629 expect(files).toEqual([
b69ab31630 expect.objectContaining({newFileName: 'deletedFile.txt', type: 'Removed'}),
b69ab31631 expect.objectContaining({newFileName: 'newFile.txt', type: 'Added'}),
b69ab31632 expect.objectContaining({newFileName: 'someFile.txt', type: 'Modified'}),
b69ab31633 expect.objectContaining({newFileName: 'some/path/foo.go', type: 'Modified'}),
b69ab31634 ]);
b69ab31635
b69ab31636 sortFilesByType(files);
b69ab31637
b69ab31638 expect(files).toEqual([
b69ab31639 expect.objectContaining({newFileName: 'some/path/foo.go', type: 'Modified'}),
b69ab31640 expect.objectContaining({newFileName: 'someFile.txt', type: 'Modified'}),
b69ab31641 expect.objectContaining({newFileName: 'newFile.txt', type: 'Added'}),
b69ab31642 expect.objectContaining({newFileName: 'deletedFile.txt', type: 'Removed'}),
b69ab31643 ]);
b69ab31644 });
b69ab31645 });
b69ab31646});