addons/isl/src/testUtils.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 {Writable} from 'shared/typeUtils';
b69ab319import type {TestingEventBus} from './TestingMessageBus';
b69ab3110import type {
b69ab3111 ClientToServerMessage,
b69ab3112 CommitInfo,
b69ab3113 Hash,
b69ab3114 Result,
b69ab3115 ServerToClientMessage,
b69ab3116 SmartlogCommits,
b69ab3117 UncommittedChanges,
b69ab3118} from './types';
b69ab3119
b69ab3120import {act, screen, waitFor, within} from '@testing-library/react';
b69ab3121import {nextTick} from 'shared/testUtils';
b69ab3122import platform from './platform';
b69ab3123import {deserializeFromString, serializeToString} from './serialize';
b69ab3124import {mostRecentSubscriptionIds} from './serverAPIState';
b69ab3125
b69ab3126const testMessageBus = platform.messageBus as TestingEventBus;
b69ab3127
b69ab3128export function simulateMessageFromServer(message: ServerToClientMessage): void {
b69ab3129 testMessageBus.simulateMessage(serializeToString(message));
b69ab3130}
b69ab3131
b69ab3132/** Filter out binary messages, and filter by wanted type. */
b69ab3133function filterMessages<K extends string>(wantedType?: K) {
b69ab3134 let messages = testMessageBus.sent
b69ab3135 .filter((msg: unknown): msg is string => !(msg instanceof ArrayBuffer))
b69ab3136 .map(deserializeFromString) as Array<ClientToServerMessage>;
b69ab3137 if (wantedType != null) {
b69ab3138 messages = messages.filter(msg => msg.type == null || msg.type === wantedType);
b69ab3139 }
b69ab3140 return messages;
b69ab3141}
b69ab3142
b69ab3143export function expectMessageSentToServer(message: Partial<ClientToServerMessage>): void {
b69ab3144 expect(filterMessages(message.type)).toContainEqual(message);
b69ab3145}
b69ab3146
b69ab3147export function getLastMessageOfTypeSentToServer<K extends string>(
b69ab3148 wantedType: K,
b69ab3149): (ClientToServerMessage & {type: K}) | undefined {
b69ab3150 const values = filterMessages(wantedType) as Array<ClientToServerMessage & {type: K}>;
b69ab3151 return values[values.length - 1];
b69ab3152}
b69ab3153
b69ab3154export function expectMessageNOTSentToServer(message: Partial<ClientToServerMessage>): void {
b69ab3155 expect(filterMessages(message.type)).not.toContainEqual(message);
b69ab3156}
b69ab3157
b69ab3158/**
b69ab3159 * Return last `num` stringified JSON messages sent to the server.
b69ab3160 */
b69ab3161export function getLastMessagesSentToServer(num: number): Array<string> {
b69ab3162 return testMessageBus.sent.slice(-num);
b69ab3163}
b69ab3164
b69ab3165export function simulateServerDisconnected(): void {
b69ab3166 testMessageBus.simulateServerStatusChange({type: 'error', error: 'server disconnected'});
b69ab3167}
b69ab3168
b69ab3169export function simulateCommits(commits: Result<SmartlogCommits>) {
b69ab3170 simulateMessageFromServer({
b69ab3171 type: 'subscriptionResult',
b69ab3172 kind: 'smartlogCommits',
b69ab3173 subscriptionID: mostRecentSubscriptionIds.smartlogCommits,
b69ab3174 data: {
b69ab3175 fetchStartTimestamp: 1,
b69ab3176 fetchCompletedTimestamp: 2,
b69ab3177 commits,
b69ab3178 },
b69ab3179 });
b69ab3180}
b69ab3181export function simulateUncommittedChangedFiles(files: Result<UncommittedChanges>) {
b69ab3182 simulateMessageFromServer({
b69ab3183 type: 'subscriptionResult',
b69ab3184 kind: 'uncommittedChanges',
b69ab3185 subscriptionID: mostRecentSubscriptionIds.uncommittedChanges,
b69ab3186 data: {
b69ab3187 fetchStartTimestamp: 1,
b69ab3188 fetchCompletedTimestamp: 2,
b69ab3189 files,
b69ab3190 },
b69ab3191 });
b69ab3192}
b69ab3193export function simulateRepoConnected(repoRoot?: string, cwd?: string) {
b69ab3194 const root = repoRoot ?? '/path/to/repo';
b69ab3195 simulateMessageFromServer({
b69ab3196 type: 'repoInfo',
b69ab3197 info: {
b69ab3198 type: 'success',
b69ab3199 repoRoot: root,
b69ab31100 dotdir: `${root}/.sl`,
b69ab31101 command: 'sl',
b69ab31102 pullRequestDomain: undefined,
b69ab31103 codeReviewSystem: {type: 'github', owner: 'owner', repo: 'repo', hostname: 'github.com'},
b69ab31104 isEdenFs: false,
b69ab31105 },
b69ab31106 cwd,
b69ab31107 });
b69ab31108 testMessageBus.simulateServerStatusChange({type: 'open'});
b69ab31109}
b69ab31110
b69ab31111export function resetTestMessages() {
b69ab31112 act(() => {
b69ab31113 testMessageBus.resetTestMessages();
b69ab31114 });
b69ab31115}
b69ab31116
b69ab31117export function commitInfoIsOpen(): boolean {
b69ab31118 return (
b69ab31119 screen.queryByTestId('commit-info-view') != null ||
b69ab31120 screen.queryByTestId('commit-info-view-loading') != null
b69ab31121 );
b69ab31122}
b69ab31123
b69ab31124export function closeCommitInfoSidebar() {
b69ab31125 if (!commitInfoIsOpen()) {
b69ab31126 return;
b69ab31127 }
b69ab31128 screen.queryAllByTestId('drawer-label').forEach(el => {
b69ab31129 const commitInfoTab = within(el).queryByText('Commit Info');
b69ab31130 commitInfoTab?.click();
b69ab31131 });
b69ab31132}
b69ab31133
b69ab31134export function openCommitInfoSidebar() {
b69ab31135 if (commitInfoIsOpen()) {
b69ab31136 return;
b69ab31137 }
b69ab31138 screen.queryAllByTestId('drawer-label').forEach(el => {
b69ab31139 const commitInfoTab = within(el).queryByText('Commit Info');
b69ab31140 commitInfoTab?.click();
b69ab31141 });
b69ab31142}
b69ab31143
b69ab31144export const emptyCommit: CommitInfo = {
b69ab31145 title: 'title',
b69ab31146 hash: '0',
b69ab31147 parents: [],
b69ab31148 grandparents: [],
b69ab31149 phase: 'draft',
b69ab31150 isDot: false,
b69ab31151 author: 'author',
b69ab31152 date: new Date(),
b69ab31153 description: '',
b69ab31154 bookmarks: [],
b69ab31155 remoteBookmarks: [],
b69ab31156 filePathsSample: [],
b69ab31157 totalFileCount: 0,
b69ab31158 maxCommonPathPrefix: '',
b69ab31159};
b69ab31160
b69ab31161export function COMMIT(
b69ab31162 hash: string,
b69ab31163 title: string,
b69ab31164 parent: Hash,
b69ab31165 info?: Partial<CommitInfo>,
b69ab31166): CommitInfo {
b69ab31167 return {
b69ab31168 ...emptyCommit,
b69ab31169 ...info,
b69ab31170 hash,
b69ab31171 title,
b69ab31172 parents: [parent],
b69ab31173 };
b69ab31174}
b69ab31175
b69ab31176/**
b69ab31177 ```
b69ab31178 | z - Commit Z
b69ab31179 | |
b69ab31180 | y - Commit Y
b69ab31181 | |
b69ab31182 | x - Commit X
b69ab31183 |/
b69ab31184 2 - another public branch (remote/master)
b69ab31185 |
b69ab31186 | e - Commit E (You are here)
b69ab31187 | |
b69ab31188 | d - Commit D
b69ab31189 | |
b69ab31190 | c - Commit C
b69ab31191 | |
b69ab31192 | b - Commit B
b69ab31193 | |
b69ab31194 | a - Commit A
b69ab31195 |/
b69ab31196 1 - some public base
b69ab31197 ```
b69ab31198*/
b69ab31199export const TEST_COMMIT_HISTORY = [
b69ab31200 COMMIT('z', 'Commit Z', 'y'),
b69ab31201 COMMIT('y', 'Commit Y', 'x'),
b69ab31202 COMMIT('x', 'Commit X', '2'),
b69ab31203 COMMIT('2', 'another public branch', '0', {phase: 'public', remoteBookmarks: ['remote/master']}),
b69ab31204 COMMIT('e', 'Commit E', 'd', {isDot: true}),
b69ab31205 COMMIT('d', 'Commit D', 'c'),
b69ab31206 COMMIT('c', 'Commit C', 'b'),
b69ab31207 COMMIT('b', 'Commit B', 'a'),
b69ab31208 COMMIT('a', 'Commit A', '1'),
b69ab31209 COMMIT('1', 'some public base', '0', {phase: 'public'}),
b69ab31210];
b69ab31211
b69ab31212export const fireMouseEvent = function (
b69ab31213 type: string,
b69ab31214 elem: EventTarget,
b69ab31215 centerX: number,
b69ab31216 centerY: number,
b69ab31217 additionalProperties?: Partial<MouseEvent | InputEvent>,
b69ab31218) {
b69ab31219 const evt = document.createEvent('MouseEvents') as Writable<MouseEvent & InputEvent>;
b69ab31220 evt.initMouseEvent(
b69ab31221 type,
b69ab31222 true,
b69ab31223 true,
b69ab31224 window,
b69ab31225 1,
b69ab31226 1,
b69ab31227 1,
b69ab31228 centerX,
b69ab31229 centerY,
b69ab31230 false,
b69ab31231 false,
b69ab31232 false,
b69ab31233 false,
b69ab31234 0,
b69ab31235 elem,
b69ab31236 );
b69ab31237 evt.dataTransfer = {} as DataTransfer;
b69ab31238 if (additionalProperties != null) {
b69ab31239 for (const [key, value] of Object.entries(additionalProperties)) {
b69ab31240 (evt as Record<string, unknown>)[key] = value;
b69ab31241 }
b69ab31242 }
b69ab31243 return elem.dispatchEvent(evt);
b69ab31244};
b69ab31245
b69ab31246export const drag = (elemDrag: HTMLElement, elemDrop: HTMLElement) => {
b69ab31247 act(() => {
b69ab31248 // calculate positions
b69ab31249 let pos = elemDrag.getBoundingClientRect();
b69ab31250 const center1X = Math.floor((pos.left + pos.right) / 2);
b69ab31251 const center1Y = Math.floor((pos.top + pos.bottom) / 2);
b69ab31252
b69ab31253 pos = elemDrop.getBoundingClientRect();
b69ab31254 const center2X = Math.floor((pos.left + pos.right) / 2);
b69ab31255 const center2Y = Math.floor((pos.top + pos.bottom) / 2);
b69ab31256
b69ab31257 // mouse over dragged element and mousedown
b69ab31258 fireMouseEvent('mousemove', elemDrag, center1X, center1Y);
b69ab31259 fireMouseEvent('mouseenter', elemDrag, center1X, center1Y);
b69ab31260 fireMouseEvent('mouseover', elemDrag, center1X, center1Y);
b69ab31261 fireMouseEvent('mousedown', elemDrag, center1X, center1Y);
b69ab31262
b69ab31263 if (!elemDrag.draggable) {
b69ab31264 return;
b69ab31265 }
b69ab31266
b69ab31267 // start dragging process over to drop target
b69ab31268 const dragStarted = fireMouseEvent('dragstart', elemDrag, center1X, center1Y);
b69ab31269 if (!dragStarted) {
b69ab31270 return;
b69ab31271 }
b69ab31272
b69ab31273 fireMouseEvent('drag', elemDrag, center1X, center1Y);
b69ab31274 fireMouseEvent('mousemove', elemDrag, center1X, center1Y);
b69ab31275 fireMouseEvent('drag', elemDrag, center2X, center2Y);
b69ab31276 fireMouseEvent('mousemove', elemDrop, center2X, center2Y);
b69ab31277
b69ab31278 // trigger dragging process on top of drop target
b69ab31279 fireMouseEvent('mouseenter', elemDrop, center2X, center2Y);
b69ab31280 fireMouseEvent('dragenter', elemDrop, center2X, center2Y);
b69ab31281 fireMouseEvent('mouseover', elemDrop, center2X, center2Y);
b69ab31282 fireMouseEvent('dragover', elemDrop, center2X, center2Y);
b69ab31283 });
b69ab31284};
b69ab31285export const drop = (elemDrag: HTMLElement, elemDrop: HTMLElement) => {
b69ab31286 act(() => {
b69ab31287 // calculate positions
b69ab31288 let pos = elemDrag.getBoundingClientRect();
b69ab31289 pos = elemDrop.getBoundingClientRect();
b69ab31290 const center2X = Math.floor((pos.left + pos.right) / 2);
b69ab31291 const center2Y = Math.floor((pos.top + pos.bottom) / 2);
b69ab31292
b69ab31293 // release dragged element on top of drop target
b69ab31294 fireMouseEvent('drop', elemDrop, center2X, center2Y);
b69ab31295 fireMouseEvent('dragend', elemDrag, center2X, center2Y);
b69ab31296 fireMouseEvent('mouseup', elemDrag, center2X, center2Y);
b69ab31297 });
b69ab31298};
b69ab31299
b69ab31300// See https://github.com/testing-library/user-event/issues/440
b69ab31301export const dragAndDrop = (elemDrag: HTMLElement, elemDrop: HTMLElement) => {
b69ab31302 drag(elemDrag, elemDrop);
b69ab31303 drop(elemDrag, elemDrop);
b69ab31304};
b69ab31305
b69ab31306export function dragAndDropCommits(
b69ab31307 draggedCommit: Hash | HTMLElement,
b69ab31308 onto: Hash,
b69ab31309 op: typeof dragAndDrop | typeof drag | typeof drop = dragAndDrop,
b69ab31310) {
b69ab31311 const draggableCommit =
b69ab31312 typeof draggedCommit !== 'string'
b69ab31313 ? draggedCommit
b69ab31314 : within(screen.getByTestId(`commit-${draggedCommit}`)).queryByTestId('draggable-commit');
b69ab31315 expect(draggableCommit).toBeDefined();
b69ab31316 const dragTargetCommit = screen.queryByTestId(`commit-${onto}`)?.querySelector('.commit-details');
b69ab31317 expect(dragTargetCommit).toBeDefined();
b69ab31318
b69ab31319 act(() => {
b69ab31320 op(draggableCommit as HTMLElement, dragTargetCommit as HTMLElement);
b69ab31321 jest.advanceTimersByTime(2);
b69ab31322 });
b69ab31323}
b69ab31324
b69ab31325export function dragCommits(draggedCommit: Hash | HTMLElement, onto: Hash) {
b69ab31326 dragAndDropCommits(draggedCommit, onto, drag);
b69ab31327}
b69ab31328
b69ab31329export function dropCommits(draggedCommit: Hash | HTMLElement, onto: Hash) {
b69ab31330 dragAndDropCommits(draggedCommit, onto, drop);
b69ab31331}
b69ab31332
b69ab31333/** Check that YouAreHere points to the given commit. */
b69ab31334export function expectYouAreHerePointAt(hash: string) {
b69ab31335 // The previous row of "hash" should be "YouAreHere".
b69ab31336 // YouAreHere
b69ab31337 // /
b69ab31338 // hash
b69ab31339 const row = screen.getByTestId(`dag-row-group-${hash}`);
b69ab31340 const previousRow = row.previousElementSibling;
b69ab31341 expect(previousRow).toHaveTextContent('You are here');
b69ab31342}
b69ab31343
b69ab31344/**
b69ab31345 * Despite catching the error in our error boundary, react + jsdom still
b69ab31346 * print big scary messages to console.warn when we throw an error.
b69ab31347 * We can ignore these during the select tests that test error boundaries.
b69ab31348 * This should be done only when needed, to prevent filtering out useful
b69ab31349 * console.error statements.
b69ab31350 * See also: https://github.com/facebook/react/issues/11098#issuecomment-412682721
b69ab31351 */
b69ab31352export function suppressReactErrorBoundaryErrorMessages() {
b69ab31353 beforeAll(() => {
b69ab31354 jest.spyOn(console, 'error').mockImplementation(() => undefined);
b69ab31355 });
b69ab31356 afterAll(() => {
b69ab31357 jest.restoreAllMocks();
b69ab31358 });
b69ab31359}
b69ab31360
b69ab31361/**
b69ab31362 * Print test name beforeEach. This can be useful to figure out which test prints
b69ab31363 * React or testing-library warnings, if the stack trace does not include the test
b69ab31364 * code.
b69ab31365 */
b69ab31366export function beforeEachPrintTestName() {
b69ab31367 beforeEach(() => {
b69ab31368 // eslint-disable-next-line no-console
b69ab31369 console.log(`Starting test: ${expect.getState().currentTestName}`);
b69ab31370 });
b69ab31371}
b69ab31372
b69ab31373/**
b69ab31374 * Drop-in replacement of `waitFor` that includes a `nextTick` to workaround
b69ab31375 * some `act()` warnings.
b69ab31376 */
b69ab31377export function waitForWithTick<T>(callback: () => Promise<T> | T): Promise<T> {
b69ab31378 return waitFor(async () => {
b69ab31379 await nextTick();
b69ab31380 return callback();
b69ab31381 });
b69ab31382}
b69ab31383
b69ab31384/**
b69ab31385 * Check that the "commit" is in the forked branch from "base".
b69ab31386 *
b69ab31387 * o <- does not return commit hashes here (not right-side)
b69ab31388 * :
b69ab31389 * | ┌─────┐
b69ab31390 * | │ o │
b69ab31391 * | │ : │
b69ab31392 * | │ o │ <- return commit hashes here (right-side branch)
b69ab31393 * | │ | │
b69ab31394 * | │ o │
b69ab31395 * | │ / │
b69ab31396 * | └─────┘
b69ab31397 * | /
b69ab31398 * base
b69ab31399 *
b69ab31400 * This is a naive implementation that does not consider merges.
b69ab31401 */
b69ab31402export function scanForkedBranchHashes(base: string): string[] {
b69ab31403 const baseRow = screen.getByTestId(`dag-row-group-${base}`);
b69ab31404 const getAttr = (e: Element, attr: string) => e.querySelector(`[${attr}]`)?.getAttribute(attr);
b69ab31405 const getNodeColumn = (row: Element) => parseInt(getAttr(row, 'data-nodecolumn') ?? '-1');
b69ab31406 const baseIndent = getNodeColumn(baseRow);
b69ab31407 // Scan rows above baseRow.
b69ab31408 let prevRow = baseRow.previousElementSibling;
b69ab31409 const branchHashes = [];
b69ab31410 while (prevRow) {
b69ab31411 const prevIndent = getNodeColumn(prevRow);
b69ab31412 if (prevIndent <= baseIndent) {
b69ab31413 // No longer right-side branch from 'base'.
b69ab31414 break;
b69ab31415 }
b69ab31416 const hash = getAttr(prevRow, 'data-commit-hash');
b69ab31417 if (hash) {
b69ab31418 branchHashes.push(hash);
b69ab31419 }
b69ab31420 prevRow = prevRow.previousElementSibling;
b69ab31421 }
b69ab31422 return branchHashes;
b69ab31423}