Skip to content

Commit c583304

Browse files
committed
Add a link to browse crate source on docs.rs
Don't show the link if there aren't any docs.rs builds as clicking on the link won't work then, but do still show the source link even if a non-docs.rs documentation link has been specified to enable review of the source exactly as crates.io serves it.
1 parent d468d7b commit c583304

File tree

4 files changed

+219
-3
lines changed

4 files changed

+219
-3
lines changed

app/components/crate-sidebar.gjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export default class CrateSidebar extends Component {
162162
</div>
163163
{{/unless}}
164164

165-
{{#if (or this.showHomepage @version.documentationLink @crate.repository)}}
165+
{{#if (or this.showHomepage @version.documentationLink @version.sourceLink @crate.repository)}}
166166
<div class='links'>
167167
{{#if this.showHomepage}}
168168
<Link @title='Homepage' @url={{@crate.homepage}} data-test-homepage-link />
@@ -172,6 +172,10 @@ export default class CrateSidebar extends Component {
172172
<Link @title='Documentation' @url={{@version.documentationLink}} data-test-docs-link />
173173
{{/if}}
174174

175+
{{#if @version.sourceLink}}
176+
<Link @title='Browse source' @url={{@version.sourceLink}} data-test-source-link />
177+
{{/if}}
178+
175179
{{#if @crate.repository}}
176180
<Link @title='Repository' @url={{@crate.repository}} data-test-repository-link />
177181
{{/if}}

app/models/version.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,9 +195,12 @@ export default class Version extends Model {
195195
return await ajax(`https://docs.rs/crate/${this.crateName}/=${this.num}/status.json`);
196196
});
197197

198+
get docsRsResponse() {
199+
return this.loadDocsStatusTask.lastSuccessful;
200+
}
201+
198202
get hasDocsRsLink() {
199-
let docsStatus = this.loadDocsStatusTask.lastSuccessful?.value;
200-
return docsStatus?.doc_status === true;
203+
return this.docsRsResponse?.value?.doc_status === true;
201204
}
202205

203206
get docsRsLink() {
@@ -228,6 +231,24 @@ export default class Version extends Model {
228231
return null;
229232
}
230233

234+
get docsRsSourceLink() {
235+
if (this.docsRsResponse) {
236+
return `https://docs.rs/crate/${this.crateName}/${this.num}/source/`;
237+
}
238+
}
239+
240+
get sourceLink() {
241+
// Return a link to docs.rs if we get any successful response from docs.rs, so that we show
242+
// the source link regardless of this crate being a library or binary, regardless of whether
243+
// the docs built successfully, and regardless of whether the build is queued or completed.
244+
let { docsRsSourceLink } = this;
245+
if (docsRsSourceLink) {
246+
return docsRsSourceLink;
247+
}
248+
249+
return null;
250+
}
251+
231252
yankTask = keepLatestTask(async () => {
232253
let data = { version: { yanked: true } };
233254
let payload = await waitForPromise(apiAction(this, { method: 'PATCH', data }));
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { expect, test } from '@/e2e/helper';
2+
import { http, HttpResponse } from 'msw';
3+
4+
test.describe('Route | crate.version | source link', { tag: '@routes' }, () => {
5+
test('show docs.rs source link even if non-docs.rs documentation link is specified', async ({ page, msw }) => {
6+
let crate = await msw.db.crate.create({ name: 'foo', documentation: 'https://foo.io/docs' });
7+
await msw.db.version.create({ crate, num: '1.0.0' });
8+
9+
await page.goto('/crates/foo');
10+
await expect(page.locator('[data-test-source-link] a')).toHaveAttribute(
11+
'href',
12+
'https://docs.rs/crate/foo/1.0.0/source/',
13+
);
14+
});
15+
16+
test('show no source link if there are no related docs.rs builds', async ({ page, msw }) => {
17+
let crate = await msw.db.crate.create({ name: 'foo' });
18+
await msw.db.version.create({ crate, num: '1.0.0' });
19+
20+
let error = HttpResponse.text('not found', { status: 404 });
21+
msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error));
22+
23+
await page.goto('/crates/foo');
24+
await expect(page.getByRole('link', { name: 'crates.io', exact: true })).toHaveCount(1);
25+
26+
await expect(page.locator('[data-test-source-link] a')).toHaveCount(0);
27+
});
28+
29+
test('show source link if `documentation` is unspecified and there are related docs.rs builds', async ({
30+
page,
31+
msw,
32+
}) => {
33+
let crate = await msw.db.crate.create({ name: 'foo' });
34+
await msw.db.version.create({ crate, num: '1.0.0' });
35+
36+
let response = HttpResponse.json({
37+
doc_status: true,
38+
version: '1.0.0',
39+
});
40+
msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response));
41+
42+
await page.goto('/crates/foo');
43+
await expect(page.locator('[data-test-source-link] a')).toHaveAttribute(
44+
'href',
45+
'https://docs.rs/crate/foo/1.0.0/source/',
46+
);
47+
});
48+
49+
test('show no source link if `documentation` points to docs.rs and there are no related docs.rs builds', async ({
50+
page,
51+
msw,
52+
}) => {
53+
let crate = await msw.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' });
54+
await msw.db.version.create({ crate, num: '1.0.0' });
55+
56+
let error = HttpResponse.text('not found', { status: 404 });
57+
msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error));
58+
59+
await page.goto('/crates/foo');
60+
await expect(page.locator('[data-test-source-link] a')).toHaveCount(0);
61+
});
62+
63+
test('show source link if `documentation` points to docs.rs and there are related docs.rs builds', async ({
64+
page,
65+
msw,
66+
}) => {
67+
let crate = await msw.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' });
68+
await msw.db.version.create({ crate, num: '1.0.0' });
69+
70+
let response = HttpResponse.json({
71+
doc_status: true,
72+
version: '1.0.0',
73+
});
74+
msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response));
75+
76+
await page.goto('/crates/foo');
77+
await expect(page.locator('[data-test-source-link] a')).toHaveAttribute(
78+
'href',
79+
'https://docs.rs/crate/foo/1.0.0/source',
80+
);
81+
});
82+
83+
test('ajax errors are ignored, but show no source link', async ({ page, msw }) => {
84+
let crate = await msw.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' });
85+
await msw.db.version.create({ crate, num: '1.0.0' });
86+
87+
let error = HttpResponse.text('error', { status: 500 });
88+
msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error));
89+
90+
await page.goto('/crates/foo');
91+
await expect(page.locator('[data-test-source-link] a')).toHaveCount(0);
92+
});
93+
94+
test('empty docs.rs responses are ignored, still show source link', async ({ page, msw }) => {
95+
let crate = await msw.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' });
96+
await msw.db.version.create({ crate, num: '0.6.2' });
97+
98+
let response = HttpResponse.json({});
99+
msw.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response));
100+
101+
await page.goto('/crates/foo');
102+
await expect(page.locator('[data-test-source-link] a')).toHaveAttribute(
103+
'href',
104+
'https://docs.rs/crate/foo/0.6.2/source/',
105+
);
106+
});
107+
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { visit } from '@ember/test-helpers';
2+
import { module, test } from 'qunit';
3+
4+
import { http, HttpResponse } from 'msw';
5+
6+
import { setupApplicationTest } from 'crates-io/tests/helpers';
7+
8+
module('Route | crate.version | source link', function (hooks) {
9+
setupApplicationTest(hooks);
10+
11+
test('shows docs.rs source link even if non-docs.rs documentation link is specified', async function (assert) {
12+
let crate = await this.db.crate.create({ name: 'foo', documentation: 'https://foo.io/docs' });
13+
await this.db.version.create({ crate, num: '1.0.0' });
14+
15+
await visit('/crates/foo');
16+
assert.dom('[data-test-source-link] a').hasAttribute('href', 'https://docs.rs/crate/foo/1.0.0/source/');
17+
});
18+
19+
test('show no source link if there are no related docs.rs builds', async function (assert) {
20+
let crate = await this.db.crate.create({ name: 'foo' });
21+
await this.db.version.create({ crate, num: '1.0.0' });
22+
23+
let error = HttpResponse.text('not found', { status: 404 });
24+
this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error));
25+
26+
await visit('/crates/foo');
27+
assert.dom('[data-test-source-link] a').doesNotExist();
28+
});
29+
30+
test('show source link if `documentation` is unspecified and there are related docs.rs builds', async function (assert) {
31+
let crate = await this.db.crate.create({ name: 'foo' });
32+
await this.db.version.create({ crate, num: '1.0.0' });
33+
34+
let response = HttpResponse.json({ doc_status: true, version: '1.0.0' });
35+
this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response));
36+
37+
await visit('/crates/foo');
38+
assert.dom('[data-test-source-link] a').hasAttribute('href', 'https://docs.rs/crate/foo/1.0.0/source/');
39+
});
40+
41+
test('show no source link if `documentation` points to docs.rs and there are no related docs.rs builds', async function (assert) {
42+
let crate = await this.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' });
43+
await this.db.version.create({ crate, num: '1.0.0' });
44+
45+
let error = HttpResponse.text('not found', { status: 404 });
46+
this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error));
47+
48+
await visit('/crates/foo');
49+
assert.dom('[data-test-source-link] a').doesNotExist();
50+
});
51+
52+
test('show source link if `documentation` points to docs.rs and there are related docs.rs builds', async function (assert) {
53+
let crate = await this.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' });
54+
await this.db.version.create({ crate, num: '1.0.0' });
55+
56+
let response = HttpResponse.json({ doc_status: true, version: '1.0.0' });
57+
this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response));
58+
59+
await visit('/crates/foo');
60+
assert.dom('[data-test-source-link] a').hasAttribute('href', 'https://docs.rs/crate/foo/1.0.0/source/');
61+
});
62+
63+
test('ajax errors are ignored, but show no source link', async function (assert) {
64+
let crate = await this.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' });
65+
await this.db.version.create({ crate, num: '1.0.0' });
66+
67+
let error = HttpResponse.text('error', { status: 500 });
68+
this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => error));
69+
70+
await visit('/crates/foo');
71+
assert.dom('[data-test-source-link] a').doesNotExist();
72+
});
73+
74+
test('empty docs.rs responses are ignored, still show source link', async function (assert) {
75+
let crate = await this.db.crate.create({ name: 'foo', documentation: 'https://docs.rs/foo/0.6.2' });
76+
await this.db.version.create({ crate, num: '0.6.2' });
77+
78+
let response = HttpResponse.json({});
79+
this.worker.use(http.get('https://docs.rs/crate/:crate/:version/status.json', () => response));
80+
81+
await visit('/crates/foo');
82+
assert.dom('[data-test-source-link] a').hasAttribute('href', 'https://docs.rs/crate/foo/0.6.2/source/');
83+
});
84+
});

0 commit comments

Comments
 (0)