{{if $v.Verified}}
{{if ne $v.SigningUser.ID 0}}
@@ -62,14 +62,14 @@
{{else}}
{{if $v.SigningKey}}
{{if ne $v.SigningKey.KeyID ""}}
- {{svg "octicon-verified" 16 "tw-mr-2"}}
+ {{svg "octicon-unverified" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:
{{$v.SigningKey.PaddedKeyID}}
{{end}}
{{end}}
{{if $v.SigningSSHKey}}
{{if ne $v.SigningSSHKey.Fingerprint ""}}
- {{svg "octicon-verified" 16 "tw-mr-2"}}
+ {{svg "octicon-unverified" 16 "tw-mr-2"}}
{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:
{{$v.SigningSSHKey.Fingerprint}}
{{end}}
diff --git a/templates/shared/user/authorlink.tmpl b/templates/shared/user/authorlink.tmpl
index abe1ab1ce2..5be8a1612f 100644
--- a/templates/shared/user/authorlink.tmpl
+++ b/templates/shared/user/authorlink.tmpl
@@ -1 +1,7 @@
-
{{.GetDisplayName}}{{if .IsBot}}
bot{{end}}
+{{if eq .ID -1}}
+
Ghost
+{{else}}
+
{{.GetDisplayName}}
+ {{if .IsBot}}
bot{{end}}
+{{end}}
diff --git a/tests/e2e/pr-review.test.e2e.ts b/tests/e2e/pr-review.test.e2e.ts
index 03f3a3d8b3..5da06e9a64 100644
--- a/tests/e2e/pr-review.test.e2e.ts
+++ b/tests/e2e/pr-review.test.e2e.ts
@@ -30,57 +30,59 @@ test('PR: Create review from files', async ({page}) => {
await save_visual(page);
});
-test('PR: Create review from commit', async ({page}) => {
- const response = await page.goto('/user2/repo1/pulls/3/commits/4a357436d925b5c974181ff12a994538ddc5a269');
- expect(response?.status()).toBe(200);
-
- await page.locator('button.add-code-comment').click();
- const code_comment = page.locator('.comment-code-cloud form textarea.markdown-text-editor');
- await expect(code_comment).toBeVisible();
-
- await code_comment.fill('This is a code comment');
- await save_visual(page);
-
- const start_button = page.locator('.comment-code-cloud form button.btn-start-review');
- // Workaround for #7152, where there might already be a pending review state from previous
- // test runs (most likely to happen when debugging tests).
- if (await start_button.isVisible({timeout: 100})) {
- await start_button.click();
- } else {
- await page.locator('.comment-code-cloud form button[name="pending_review"]').click();
- }
-
- await expect(page.locator('.comment-list .comment-container')).toBeVisible();
-
- // We need to wait for the review to be processed. Checking the comment counter
- // conveniently does that.
- await expect(page.locator('#review-box .js-btn-review > span.review-comments-counter')).toHaveText('1');
-
- await page.locator('#review-box .js-btn-review').click();
- await expect(page.locator('.tippy-box .review-box-panel')).toBeVisible();
- await save_visual(page);
-
- await page.locator('.review-box-panel textarea.markdown-text-editor')
- .fill('This is a review');
- await page.locator('.review-box-panel button.btn-submit[value="approve"]').click();
- await page.waitForURL(/.*\/user2\/repo1\/pulls\/3#issuecomment-\d+/);
- await save_visual(page);
-
- // In addition to testing the ability to delete comments, this also
- // performs clean up. If tests are run for multiple platforms, the data isn't reset
- // in-between, and subsequent runs of this test would fail, because when there already is
- // a comment, the on-hover button to start a conversation doesn't appear anymore.
- await page.goto('/user2/repo1/pulls/3/commits/4a357436d925b5c974181ff12a994538ddc5a269');
- await page.locator('.comment-header-right.actions a.context-menu').click();
-
- await expect(page.locator('.comment-header-right.actions div.menu').getByText(/Copy link.*/)).toBeVisible();
- // The button to delete a comment will prompt for confirmation using a browser alert.
- page.on('dialog', (dialog) => dialog.accept());
- await page.locator('.comment-header-right.actions div.menu .delete-comment').click();
-
- await expect(page.locator('.comment-list .comment-container')).toBeHidden();
- await save_visual(page);
-});
+// Test disabled because it is flaky.
+// TODO: re-enable it
+// test('PR: Create review from commit', async ({page}) => {
+// const response = await page.goto('/user2/repo1/pulls/3/commits/4a357436d925b5c974181ff12a994538ddc5a269');
+// expect(response?.status()).toBe(200);
+//
+// await page.locator('button.add-code-comment').click();
+// const code_comment = page.locator('.comment-code-cloud form textarea.markdown-text-editor');
+// await expect(code_comment).toBeVisible();
+//
+// await code_comment.fill('This is a code comment');
+// await save_visual(page);
+//
+// const start_button = page.locator('.comment-code-cloud form button.btn-start-review');
+// // Workaround for #7152, where there might already be a pending review state from previous
+// // test runs (most likely to happen when debugging tests).
+// if (await start_button.isVisible({timeout: 100})) {
+// await start_button.click();
+// } else {
+// await page.locator('.comment-code-cloud form button[name="pending_review"]').click();
+// }
+//
+// await expect(page.locator('.comment-list .comment-container')).toBeVisible();
+//
+// // We need to wait for the review to be processed. Checking the comment counter
+// // conveniently does that.
+// await expect(page.locator('#review-box .js-btn-review > span.review-comments-counter')).toHaveText('1');
+//
+// await page.locator('#review-box .js-btn-review').click();
+// await expect(page.locator('.tippy-box .review-box-panel')).toBeVisible();
+// await save_visual(page);
+//
+// await page.locator('.review-box-panel textarea.markdown-text-editor')
+// .fill('This is a review');
+// await page.locator('.review-box-panel button.btn-submit[value="approve"]').click();
+// await page.waitForURL(/.*\/user2\/repo1\/pulls\/3#issuecomment-\d+/);
+// await save_visual(page);
+//
+// // In addition to testing the ability to delete comments, this also
+// // performs clean up. If tests are run for multiple platforms, the data isn't reset
+// // in-between, and subsequent runs of this test would fail, because when there already is
+// // a comment, the on-hover button to start a conversation doesn't appear anymore.
+// await page.goto('/user2/repo1/pulls/3/commits/4a357436d925b5c974181ff12a994538ddc5a269');
+// await page.locator('.comment-header-right.actions a.context-menu').click();
+//
+// await expect(page.locator('.comment-header-right.actions div.menu').getByText(/Copy link.*/)).toBeVisible();
+// // The button to delete a comment will prompt for confirmation using a browser alert.
+// page.on('dialog', (dialog) => dialog.accept());
+// await page.locator('.comment-header-right.actions div.menu .delete-comment').click();
+//
+// await expect(page.locator('.comment-list .comment-container')).toBeHidden();
+// await save_visual(page);
+// });
test('PR: Navigate by single commit', async ({page}) => {
const response = await page.goto('/user2/repo1/pulls/3/commits');
diff --git a/tests/integration/actions_route_test.go b/tests/integration/actions_route_test.go
index 521b379064..c058877806 100644
--- a/tests/integration/actions_route_test.go
+++ b/tests/integration/actions_route_test.go
@@ -146,38 +146,3 @@ func TestActionsWebRouteLatestRun(t *testing.T) {
assert.Equal(t, workflow.HTMLURL(), resp.Header().Get("Location"))
})
}
-
-func TestActionsWebRouteArtifactDeletion(t *testing.T) {
- onGiteaRun(t, func(t *testing.T, u *url.URL) {
- user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
-
- // create the repo
- repo, _, f := tests.CreateDeclarativeRepo(t, user2, "",
- []unit_model.Type{unit_model.TypeActions}, nil,
- []*files_service.ChangeRepoFile{
- {
- Operation: "create",
- TreePath: ".gitea/workflows/pr.yml",
- ContentReader: strings.NewReader("name: test\non:\n push:\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"),
- },
- },
- )
- defer f()
-
- // a run has been created
- assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
-
- // Load the run we just created
- run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID})
- err := run.LoadAttributes(t.Context())
- require.NoError(t, err)
-
- // Visit it's web view
- req := NewRequest(t, "GET", run.HTMLURL())
- resp := MakeRequest(t, req, http.StatusOK)
- htmlDoc := NewHTMLParser(t, resp.Body)
-
- // Assert that the artifact deletion markup exists
- htmlDoc.AssertElement(t, "[data-locale-confirm-delete-artifact]", true)
- })
-}
diff --git a/tests/integration/actions_view_test.go b/tests/integration/actions_view_test.go
new file mode 100644
index 0000000000..14ef123674
--- /dev/null
+++ b/tests/integration/actions_view_test.go
@@ -0,0 +1,114 @@
+// Copyright 2025 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package integration
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+
+ actions_model "forgejo.org/models/actions"
+ unit_model "forgejo.org/models/unit"
+ "forgejo.org/models/unittest"
+ user_model "forgejo.org/models/user"
+ files_service "forgejo.org/services/repository/files"
+ "forgejo.org/tests"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestActionsViewArtifactDeletion(t *testing.T) {
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ // create the repo
+ repo, _, f := tests.CreateDeclarativeRepo(t, user2, "",
+ []unit_model.Type{unit_model.TypeActions}, nil,
+ []*files_service.ChangeRepoFile{
+ {
+ Operation: "create",
+ TreePath: ".gitea/workflows/pr.yml",
+ ContentReader: strings.NewReader("name: test\non:\n push:\njobs:\n test:\n runs-on: ubuntu-latest\n steps:\n - run: echo helloworld\n"),
+ },
+ },
+ )
+ defer f()
+
+ // a run has been created
+ assert.Equal(t, 1, unittest.GetCount(t, &actions_model.ActionRun{RepoID: repo.ID}))
+
+ // Load the run we just created
+ run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: repo.ID})
+ err := run.LoadAttributes(t.Context())
+ require.NoError(t, err)
+
+ // Visit it's web view
+ req := NewRequest(t, "GET", run.HTMLURL())
+ resp := MakeRequest(t, req, http.StatusOK)
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ // Assert that the artifact deletion markup exists
+ htmlDoc.AssertElement(t, "[data-locale-confirm-delete-artifact]", true)
+ })
+}
+
+func TestActionViewsArtifactDownload(t *testing.T) {
+ defer prepareTestEnvActionsArtifacts(t)()
+
+ assertDataAttrs := func(t *testing.T, body *bytes.Buffer, runID int64) {
+ t.Helper()
+ run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: runID})
+ htmlDoc := NewHTMLParser(t, body)
+ selector := "#repo-action-view"
+ htmlDoc.AssertAttrEqual(t, selector, "data-run-id", fmt.Sprintf("%d", run.ID))
+ htmlDoc.AssertAttrEqual(t, selector, "data-run-index", fmt.Sprintf("%d", run.Index))
+ }
+
+ t.Run("V3", func(t *testing.T) {
+ runIndex := 187
+ runID := int64(791)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/user5/repo4/actions/runs/%d/artifacts", runIndex))
+ resp := MakeRequest(t, req, http.StatusOK)
+ assert.JSONEq(t, `{"artifacts":[{"name":"multi-file-download","size":2048,"status":"completed"}]}`, strings.TrimSuffix(resp.Body.String(), "\n"))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/user5/repo4/actions/runs/%d", runIndex))
+ resp = MakeRequest(t, req, http.StatusOK)
+ assertDataAttrs(t, resp.Body, runID)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/user5/repo4/actions/runs/%d/artifacts/multi-file-download", runID))
+ resp = MakeRequest(t, req, http.StatusOK)
+ assert.Contains(t, resp.Header().Get("content-disposition"), "multi-file-download.zip")
+ })
+
+ t.Run("V4", func(t *testing.T) {
+ runIndex := 188
+ runID := int64(792)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/user5/repo4/actions/runs/%d/artifacts", runIndex))
+ resp := MakeRequest(t, req, http.StatusOK)
+ assert.JSONEq(t, `{"artifacts":[{"name":"artifact-v4-download","size":1024,"status":"completed"}]}`, strings.TrimSuffix(resp.Body.String(), "\n"))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/user5/repo4/actions/runs/%d", runIndex))
+ resp = MakeRequest(t, req, http.StatusOK)
+ assertDataAttrs(t, resp.Body, runID)
+
+ download := fmt.Sprintf("/user5/repo4/actions/runs/%d/artifacts/artifact-v4-download", runID)
+ req = NewRequest(t, "GET", download)
+ resp = MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, "bytes", resp.Header().Get("accept-ranges"))
+ assert.Contains(t, resp.Header().Get("content-disposition"), "artifact-v4-download.zip")
+ assert.Equal(t, strings.Repeat("D", 1024), resp.Body.String())
+
+ // Partial artifact download
+ req = NewRequest(t, "GET", download).SetHeader("range", "bytes=0-99")
+ resp = MakeRequest(t, req, http.StatusPartialContent)
+ assert.Equal(t, "bytes 0-99/1024", resp.Header().Get("content-range"))
+ assert.Equal(t, strings.Repeat("D", 100), resp.Body.String())
+ })
+}
diff --git a/tests/integration/api_actions_artifact_test.go b/tests/integration/api_actions_artifact_test.go
index 4ecbbeb92c..1526ae3585 100644
--- a/tests/integration/api_actions_artifact_test.go
+++ b/tests/integration/api_actions_artifact_test.go
@@ -267,11 +267,6 @@ func TestActionsArtifactDownloadMultiFiles(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, strings.Repeat(bodyChar, 1024), resp.Body.String())
}
-
- // Download artifact via user-facing URL
- req = NewRequest(t, "GET", "/user5/repo4/actions/runs/187/artifacts/multi-file-download")
- resp = MakeRequest(t, req, http.StatusOK)
- assert.Contains(t, resp.Header().Get("content-disposition"), "multi-file-download.zip")
}
func TestActionsArtifactUploadWithRetentionDays(t *testing.T) {
diff --git a/tests/integration/api_actions_artifact_v4_test.go b/tests/integration/api_actions_artifact_v4_test.go
index 716d9e1abe..02324ee73b 100644
--- a/tests/integration/api_actions_artifact_v4_test.go
+++ b/tests/integration/api_actions_artifact_v4_test.go
@@ -339,20 +339,6 @@ func TestActionsArtifactV4DownloadSingle(t *testing.T) {
body := strings.Repeat("D", 1024)
assert.Equal(t, "bytes", resp.Header().Get("accept-ranges"))
assert.Equal(t, body, resp.Body.String())
-
- // Download artifact via user-facing URL
- req = NewRequest(t, "GET", "/user5/repo4/actions/runs/188/artifacts/artifact-v4-download")
- resp = MakeRequest(t, req, http.StatusOK)
- assert.Equal(t, "bytes", resp.Header().Get("accept-ranges"))
- assert.Contains(t, resp.Header().Get("content-disposition"), "artifact-v4-download.zip")
- assert.Equal(t, body, resp.Body.String())
-
- // Partial artifact download
- req = NewRequest(t, "GET", "/user5/repo4/actions/runs/188/artifacts/artifact-v4-download").SetHeader("range", "bytes=0-99")
- resp = MakeRequest(t, req, http.StatusPartialContent)
- body = strings.Repeat("D", 100)
- assert.Equal(t, "bytes 0-99/1024", resp.Header().Get("content-range"))
- assert.Equal(t, body, resp.Body.String())
}
func TestActionsArtifactV4DownloadRange(t *testing.T) {
@@ -378,12 +364,6 @@ func TestActionsArtifactV4DownloadRange(t *testing.T) {
resp = MakeRequest(t, req, http.StatusPartialContent)
assert.Equal(t, "bytes 100-199/1024", resp.Header().Get("content-range"))
assert.Equal(t, bstr, resp.Body.String())
-
- // Download (user-facing API)
- req = NewRequest(t, "GET", "/user5/repo4/actions/runs/188/artifacts/artifact-v4-download").SetHeader("range", "bytes=100-199")
- resp = MakeRequest(t, req, http.StatusPartialContent)
- assert.Equal(t, "bytes 100-199/1024", resp.Header().Get("content-range"))
- assert.Equal(t, bstr, resp.Body.String())
}
func TestActionsArtifactV4Delete(t *testing.T) {
diff --git a/tests/integration/html_helper.go b/tests/integration/html_helper.go
index 0caa50d0c0..9758ace694 100644
--- a/tests/integration/html_helper.go
+++ b/tests/integration/html_helper.go
@@ -26,6 +26,17 @@ func NewHTMLParser(t testing.TB, body *bytes.Buffer) *HTMLDoc {
return &HTMLDoc{doc: doc}
}
+func (doc *HTMLDoc) AssertAttrEqual(t testing.TB, selector, attr, expected string) bool {
+ t.Helper()
+ selection := doc.doc.Find(selector)
+ require.NotEmpty(t, selection, selector)
+
+ actual, exists := selection.Attr(attr)
+ require.True(t, exists, "%s not found in %s", attr, selection.Text())
+
+ return assert.Equal(t, expected, actual)
+}
+
// GetInputValueByID for get input value by id
func (doc *HTMLDoc) GetInputValueByID(id string) string {
text, _ := doc.doc.Find("#" + id).Attr("value")
diff --git a/tests/integration/repo_tag_test.go b/tests/integration/repo_tag_test.go
index 4f72b58d12..b119f5d917 100644
--- a/tests/integration/repo_tag_test.go
+++ b/tests/integration/repo_tag_test.go
@@ -32,12 +32,14 @@ func TestTagViewWithoutRelease(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+ session := loginUser(t, owner.Name)
+
err := release.CreateNewTag(git.DefaultContext, owner, repo, "master", "no-release", "release-less tag")
require.NoError(t, err)
// Test that the page loads
req := NewRequestf(t, "GET", "/%s/releases/tag/no-release", repo.FullName())
- resp := MakeRequest(t, req, http.StatusOK)
+ resp := session.MakeRequest(t, req, http.StatusOK)
// Test that the tags sub-menu is active and has a counter
htmlDoc := NewHTMLParser(t, resp.Body)
@@ -54,6 +56,33 @@ func TestTagViewWithoutRelease(t *testing.T) {
// Test that there is no "Stable" link
htmlDoc.AssertElement(t, "h4.release-list-title > span.ui.green.label", false)
+
+ // Ensure that there is no "Edit" button
+ htmlDoc.AssertElement(t, ".detail a.muted > svg.octicon-pencil", false)
+
+ // Test that the correct user is linked
+ ownerLinkHref, _ := htmlDoc.Find("a.author").Attr("href")
+ assert.Equal(t, "/user2", ownerLinkHref)
+
+ t.Run("Ghost owner", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ ghost := user_model.NewGhostUser()
+ err = release.CreateNewTag(git.DefaultContext, ghost, repo, "master", "ghost-tag", "a spooky tag")
+ require.NoError(t, err)
+
+ req := NewRequestf(t, "GET", "/%s/releases/tag/ghost-tag", repo.FullName())
+ resp := session.MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+
+ // Test that the Ghost user does not link anywhere
+ ownerLink := htmlDoc.Find("a.author")
+ _, ok := ownerLink.Attr("href")
+ assert.Equal(t, 1, ownerLink.Length())
+ assert.False(t, ok)
+ assert.Equal(t, "Ghost", ownerLink.Text())
+ })
}
func TestCreateNewTagProtected(t *testing.T) {
diff --git a/web_src/js/components/RepoActionView.test.js b/web_src/js/components/RepoActionView.test.js
index 0b9527ced1..9694d34b02 100644
--- a/web_src/js/components/RepoActionView.test.js
+++ b/web_src/js/components/RepoActionView.test.js
@@ -215,3 +215,104 @@ test('load multiple steps on a finished action', async () => {
expect(wrapper.get('.job-step-section:nth-of-type(2) .job-log-line:nth-of-type(2) .log-msg').text()).toEqual('Step #2 Log #2');
expect(wrapper.get('.job-step-section:nth-of-type(2) .job-log-line:nth-of-type(3) .log-msg').text()).toEqual('Step #2 Log #3');
});
+
+test('artifacts download links', async () => {
+ Object.defineProperty(document.documentElement, 'lang', {value: 'en'});
+ vi.spyOn(global, 'fetch').mockImplementation((url, opts) => {
+ if (url.endsWith('/artifacts')) {
+ return Promise.resolve({
+ ok: true,
+ json: vi.fn().mockResolvedValue(
+ {
+ artifacts: [
+ {name: 'artifactname1', size: 111, status: 'completed'},
+ {name: 'artifactname2', size: 222, status: 'expired'},
+ ],
+ },
+ ),
+ });
+ }
+
+ const postBody = JSON.parse(opts.body);
+ const stepsLog_value = [];
+ for (const cursor of postBody.logCursors) {
+ if (cursor.expanded) {
+ stepsLog_value.push(
+ {
+ step: cursor.step,
+ cursor: 0,
+ lines: [
+ {index: 1, message: `Step #${cursor.step + 1} Log #1`, timestamp: 0},
+ ],
+ },
+ );
+ }
+ }
+ const jobs_value = {
+ state: {
+ run: {
+ status: 'success',
+ commit: {
+ pusher: {},
+ },
+ },
+ currentJob: {
+ steps: [
+ {
+ summary: 'Test Step #1',
+ duration: '1s',
+ status: 'success',
+ },
+ ],
+ },
+ },
+ logs: {
+ stepsLog: opts.body?.includes('"cursor":null') ? stepsLog_value : [],
+ },
+ };
+
+ return Promise.resolve({
+ ok: true,
+ json: vi.fn().mockResolvedValue(
+ jobs_value,
+ ),
+ });
+ });
+
+ const wrapper = mount(RepoActionView, {
+ props: {
+ actionsURL: 'https://example.com/example-org/example-repo/actions',
+ runIndex: '10',
+ runID: '1001',
+ jobIndex: '2',
+ locale: {
+ approve: '',
+ cancel: '',
+ rerun: '',
+ artifactsTitle: 'artifactTitleHere',
+ areYouSure: '',
+ confirmDeleteArtifact: '',
+ rerun_all: '',
+ showTimeStamps: '',
+ showLogSeconds: '',
+ showFullScreen: '',
+ downloadLogs: '',
+ status: {
+ unknown: '',
+ waiting: '',
+ running: '',
+ success: '',
+ failure: '',
+ cancelled: '',
+ skipped: '',
+ blocked: '',
+ },
+ },
+ },
+ });
+ await flushPromises();
+
+ expect(wrapper.get('.job-artifacts .job-artifacts-title').text()).toEqual('artifactTitleHere');
+ expect(wrapper.get('.job-artifacts .job-artifacts-item:nth-of-type(1) .job-artifacts-link').attributes('href')).toEqual('https://example.com/example-org/example-repo/actions/runs/1001/artifacts/artifactname1');
+ expect(wrapper.get('.job-artifacts .job-artifacts-item:nth-of-type(2) .job-artifacts-link').attributes('href')).toEqual('https://example.com/example-org/example-repo/actions/runs/1001/artifacts/artifactname2');
+});
diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue
index 4d34960e46..3f89bc9806 100644
--- a/web_src/js/components/RepoActionView.vue
+++ b/web_src/js/components/RepoActionView.vue
@@ -15,6 +15,7 @@ const sfc = {
},
props: {
runIndex: String,
+ runID: String,
jobIndex: String,
actionsURL: String,
workflowName: String,
@@ -386,6 +387,7 @@ export function initRepositoryActionView() {
const view = createApp(sfc, {
runIndex: el.getAttribute('data-run-index'),
+ runID: el.getAttribute('data-run-id'),
jobIndex: el.getAttribute('data-job-index'),
actionsURL: el.getAttribute('data-actions-url'),
workflowName: el.getAttribute('data-workflow-name'),
@@ -473,7 +475,7 @@ export function initRepositoryActionView() {