Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions webviews/common/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,10 @@ body img.avatar {
margin-right: 0;
}

.status-check-link-placeholder {
visibility: hidden;
}

.automerge-section {
display: flex;
}
Expand Down
14 changes: 6 additions & 8 deletions webviews/components/merge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -705,14 +705,12 @@ const StatusCheckDetails = ({ statuses }: { statuses: PullRequestCheckStatus[] }
<a href={s.targetUrl} title={s.targetUrl}>
Details
</a>
) : null}
{s.isCheckRun && s.databaseId ? (
s.state === CheckState.Failure ? (
<button className="icon-button" title="View Logs" onClick={() => handleViewLogs(s)}>
{loadingLogId === s.id ? loadingIcon : outputIcon}
</button>
) : <span className="view-check-logs-placeholder" />
) : null}
) : <span className="status-check-link-placeholder" aria-hidden="true">Details</span>}
Comment thread
alexr00 marked this conversation as resolved.
{s.isCheckRun && s.databaseId && s.state === CheckState.Failure ? (
<button className="icon-button" title="View Logs" onClick={() => handleViewLogs(s)}>
{loadingLogId === s.id ? loadingIcon : outputIcon}
</button>
) : <span className="view-check-logs-placeholder" aria-hidden="true" />}
</div>
</div>
))}
Expand Down
3 changes: 2 additions & 1 deletion webviews/editorWebview/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,9 @@ body .comment-container .review-comment-header {
gap: 4px;
}

.status-check .icon-button,
.view-check-logs-placeholder {
width: 20px;
flex: 0 0 22px;
}

#merge-on-github {
Expand Down
107 changes: 105 additions & 2 deletions webviews/editorWebview/test/overview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
*--------------------------------------------------------------------------------------------*/

import { default as assert } from 'assert';
import { readFileSync } from 'fs';
import * as path from 'path';
import * as React from 'react';
import { cleanup, fireEvent, render, wait, waitForElement } from 'react-testing-library';
import { createSandbox, SinonSandbox } from 'sinon';

import { GithubItemStateEnum, PullRequestMergeability } from '../../../src/github/interface';
import { PullRequestBuilder } from './builder/pullRequest';
import { CheckState, GithubItemStateEnum, PullRequestCheckStatus, PullRequestMergeability } from '../../../src/github/interface';
import { Overview as ActivityBarOverview } from '../../activityBarView/overview';
import { PRContext, default as PullRequestContext } from '../../common/context';
import { Overview } from '../overview';
import { PullRequestBuilder } from './builder/pullRequest';

describe('Overview', function () {
let sinon: SinonSandbox;
Expand Down Expand Up @@ -60,6 +63,106 @@ describe('Overview', function () {
assert.strictEqual(openOnGitHub.callCount, 2);
});

it('reserves details and log action slots for every status check', async function () {
const cases: Pick<PullRequestCheckStatus, 'state' | 'isCheckRun' | 'databaseId'>[] = [
{ state: CheckState.Failure, isCheckRun: false, databaseId: undefined },
{ state: CheckState.Failure, isCheckRun: true, databaseId: 1 },
{ state: CheckState.Pending, isCheckRun: true, databaseId: 2 },
{ state: CheckState.Success, isCheckRun: true, databaseId: 3 },
{ state: CheckState.Neutral, isCheckRun: true, databaseId: 4 },
{ state: CheckState.Unknown, isCheckRun: true, databaseId: 5 },
{ state: CheckState.Failure, isCheckRun: true, databaseId: null },
{ state: CheckState.Failure, isCheckRun: true, databaseId: undefined },
{ state: CheckState.Failure, isCheckRun: true, databaseId: 0 },
];
const statuses: PullRequestCheckStatus[] = cases.map((check, index) => ({
...check,
id: `check-${index}`,
context: `Check ${index}`,
description: null,
workflowName: undefined,
event: undefined,
url: undefined,
avatarUrl: undefined,
targetUrl: index === cases.length - 1 ? null : `https://example.com/checks/${index}`,
isRequired: index % 2 === 0,
}));
const pr = new PullRequestBuilder().status(status => status.state(CheckState.Failure).statuses(statuses)).build();
const context = new PRContext(pr);
const viewCheckLogs = sinon.stub(context, 'viewCheckLogs').resolves();
const out = render(
<PullRequestContext.Provider value={context}>
<Overview {...pr} />
</PullRequestContext.Provider>,
);

const rows = out.container.querySelectorAll('.status-check');
assert.strictEqual(rows.length, statuses.length);
for (const status of statuses) {
const row = [...rows].find(row => row.querySelector('.status-check-detail-text')?.textContent?.trim() === status.context);
assert(row);
const actions = row.lastElementChild;
assert(actions);
assert.strictEqual(actions.querySelector('.label')?.textContent ?? null, status.isRequired ? 'Required' : null);
assert.strictEqual(actions.querySelector('a')?.getAttribute('href') ?? null, status.targetUrl);
const linkPlaceholder = actions.querySelector('.status-check-link-placeholder');
if (status.targetUrl) {
assert.strictEqual(linkPlaceholder, null);
} else {
assert(linkPlaceholder);
assert.strictEqual(linkPlaceholder.textContent, 'Details');
assert.strictEqual(linkPlaceholder.getAttribute('aria-hidden'), 'true');
}
const slot = actions.lastElementChild;
assert(slot);
if (status.isCheckRun && status.databaseId && status.state === CheckState.Failure) {
assert.strictEqual(slot.getAttribute('title'), 'View Logs');
assert.strictEqual(actions.querySelector('.view-check-logs-placeholder'), null);
fireEvent.click(slot);
await wait(() => assert(viewCheckLogs.calledOnceWithExactly(status)));
} else {
assert(slot.classList.contains('view-check-logs-placeholder'));
assert.strictEqual(slot.getAttribute('aria-hidden'), 'true');
assert.strictEqual(actions.querySelector('button'), null);
}
}
assert.strictEqual(viewCheckLogs.callCount, 1);
});

it('keeps Details placeholders invisible in both PR overviews using shared styles', function () {
const status: PullRequestCheckStatus = {
id: 'missing-details', state: CheckState.Failure, context: 'Check without details',
description: null, targetUrl: null, workflowName: undefined, event: undefined,
url: undefined, avatarUrl: undefined, isRequired: false, isCheckRun: false, databaseId: undefined,
};
const pr = new PullRequestBuilder().status(checks => checks.state(CheckState.Failure).statuses([status])).build();
const sharedCss = readFileSync(path.resolve('webviews', 'common', 'common.css'), 'utf8');
const placeholderRule = /^\.status-check-link-placeholder\s*\{[^}]*\}/m.exec(sharedCss);
assert(placeholderRule);
const sharedStyles = document.createElement('style');
sharedStyles.textContent = placeholderRule[0];
document.head.appendChild(sharedStyles);
try {
for (const Component of [Overview, ActivityBarOverview]) {
const out = render(
<PullRequestContext.Provider value={new PRContext(pr)}>
<Component {...pr} />
</PullRequestContext.Provider>,
);
const placeholder = out.container.querySelector('.status-check-link-placeholder');
assert(placeholder);
assert.strictEqual(placeholder.textContent, 'Details');
assert.strictEqual(placeholder.getAttribute('aria-hidden'), 'true');
const style = window.getComputedStyle(placeholder);
assert.strictEqual(style.visibility, 'hidden');
assert.notStrictEqual(style.display, 'none');
out.unmount();
}
} finally {
sharedStyles.remove();
}
});

it('shows the stack position and ordered pull requests in the merge section', async function () {
const pr = new PullRequestBuilder().number(794).stack({
position: 2,
Expand Down
Loading