From ad3bfbc6868755b464ec4674869e6d8d207ce5f0 Mon Sep 17 00:00:00 2001 From: William Allen Date: Sun, 16 Aug 2026 14:43:56 -0400 Subject: [PATCH] Add timeline card to build summary page This PR adds a "timeline" card to the build summary page, continuing our effort to redesign this page. --- .../js/vue/components/BuildSummaryPage.vue | 24 +- .../BuildSummaryPage/BuildTimelineCard.vue | 432 ++++++++++++++++++ tests/Browser/Pages/BuildSummaryPageTest.php | 243 ++++++++++ 3 files changed, 684 insertions(+), 15 deletions(-) create mode 100644 resources/js/vue/components/BuildSummaryPage/BuildTimelineCard.vue diff --git a/resources/js/vue/components/BuildSummaryPage.vue b/resources/js/vue/components/BuildSummaryPage.vue index 4aef59646f..1c93385fdc 100644 --- a/resources/js/vue/components/BuildSummaryPage.vue +++ b/resources/js/vue/components/BuildSummaryPage.vue @@ -10,6 +10,8 @@ + +
import { - faQuestionCircle, faLink, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import LoadingIndicator from './shared/LoadingIndicator.vue'; import BuildSummaryCard from './shared/BuildSummaryCard.vue'; import BuildSidebar from './shared/BuildSidebar.vue'; +import BuildTimelineCard from './BuildSummaryPage/BuildTimelineCard.vue'; import gql from 'graphql-tag'; -import Utils from './shared/Utils'; import BuildTimeChart from './BuildSummaryPage/BuildTimeChart.vue'; import { DateTime, Duration } from 'luxon'; export default { name: 'BuildSummaryPage', - components: { BuildTimeChart, BuildSummaryCard, LoadingIndicator, BuildSidebar, FontAwesomeIcon }, + components: { BuildTimelineCard, BuildTimeChart, BuildSummaryCard, LoadingIndicator, BuildSidebar, FontAwesomeIcon }, props: { projectId: { @@ -486,10 +487,6 @@ export default { type: String, default: '', }, - userId: { - type: Number, - default: 0, - }, }, data() { @@ -509,6 +506,9 @@ export default { id name startTime + configureDuration + buildDuration + testDuration buildType configureErrorsCount configureWarningsCount @@ -516,6 +516,7 @@ export default { buildWarningsCount failedTestsCount notRunTestsCount + passedTestsCount site { id name @@ -617,12 +618,10 @@ export default { this.cdash.test = { nfailed: Math.max(0, build.failedTestsCount), nnotrun: Math.max(0, build.notRunTestsCount), + npassed: Math.max(0, build.passedTestsCount), }; this.cdash.projectname_encoded = encodeURIComponent(build.project.name); - this.cdash.user = { - id: this.userId, - }; }, error(error) { this.errored = true; @@ -674,15 +673,10 @@ export default { computed: { FA() { return { - faQuestionCircle, faLink, }; }, - Utils() { - return Utils; - }, - isParentBuild() { return !(this.buildData && this.buildData.subProject); }, diff --git a/resources/js/vue/components/BuildSummaryPage/BuildTimelineCard.vue b/resources/js/vue/components/BuildSummaryPage/BuildTimelineCard.vue new file mode 100644 index 0000000000..f09dbc5516 --- /dev/null +++ b/resources/js/vue/components/BuildSummaryPage/BuildTimelineCard.vue @@ -0,0 +1,432 @@ + + + diff --git a/tests/Browser/Pages/BuildSummaryPageTest.php b/tests/Browser/Pages/BuildSummaryPageTest.php index a29d52dc0f..8a8c7d5de5 100644 --- a/tests/Browser/Pages/BuildSummaryPageTest.php +++ b/tests/Browser/Pages/BuildSummaryPageTest.php @@ -3,10 +3,12 @@ namespace Tests\Browser\Pages; use App\Models\Build; +use App\Models\Configure; use App\Models\Project; use App\Models\Site; use App\Models\SiteInformation; use App\Services\SiteService; +use Illuminate\Support\Carbon; use Illuminate\Support\Str; use Laravel\Dusk\Browser; use Tests\BrowserTestCase; @@ -80,4 +82,245 @@ public function testShowsHistoryLink(): void ; }); } + + public function testBuildTimelineShowsAllStages(): void + { + $startTime = Carbon::now()->subHours(2); + $endTime = Carbon::now()->subHour(); + $submitTime = Carbon::now()->subMinutes(30); + + /** @var Build $build */ + $build = $this->project->builds()->create([ + 'siteid' => $this->site->id, + 'name' => Str::uuid()->toString(), + 'uuid' => Str::uuid()->toString(), + 'starttime' => $startTime->toIso8601String(), + 'endtime' => $endTime->toIso8601String(), + 'submittime' => $submitTime->toIso8601String(), + 'configureerrors' => 0, + 'configurewarnings' => 0, + 'builderrors' => 0, + 'buildwarnings' => 0, + 'testpassed' => 10, + 'testfailed' => 0, + 'testnotrun' => 0, + 'configureduration' => 60, + 'buildduration' => 300, + 'testduration' => 120, + ]); + + $build->configure()->create(Configure::factory()->make()->toArray()); + + $this->browse(function (Browser $browser) use ($submitTime, $endTime, $startTime, $build): void { + $browser->resize(1920, 1080) + ->visit("/builds/{$build->id}") + ->waitFor('@build-timeline') + ->within('@build-timeline', function (Browser $timeline) use ($submitTime, $endTime, $startTime): void { + $timeline->waitForText('Start') + ->waitForText('Configure') + ->waitForText('Build') + ->waitForText('Test') + ->waitForText('End') + ->waitForText('Submit') + ->assertSee('Start') + ->assertSee('Configure') + ->assertSee('Build') + ->assertSee('Test') + ->assertSee('End') + ->assertSee('Submit') + ->assertSee('1 min') // Configure 60s + ->assertSee('5 min') // Build 300s + ->assertSee('2 min') // Test 120s + ->assertSee($startTime->format('F j, Y')) + ->assertSee($startTime->format('g:i:s A')) + ->assertSee($endTime->format('g:i:s A')) + ->assertSee($submitTime->format('g:i:s A')); + }); + }); + } + + public function testBuildTimelineHidesMissingStages(): void + { + $startTime = Carbon::now()->subHours(2); + $endTime = Carbon::now()->subHour(); + + /** @var Build $build */ + $build = $this->project->builds()->create([ + 'siteid' => $this->site->id, + 'name' => Str::uuid()->toString(), + 'uuid' => Str::uuid()->toString(), + 'starttime' => $startTime->toIso8601String(), + 'endtime' => $endTime->toIso8601String(), + 'configureerrors' => -1, + 'builderrors' => -1, + 'testfailed' => -1, + ]); + + $this->browse(function (Browser $browser) use ($build): void { + $browser->resize(1920, 1080) + ->visit("/builds/{$build->id}") + ->waitFor('@build-timeline') + ->within('@build-timeline', function (Browser $timeline): void { + $timeline->waitForText('Start') + ->waitForText('End') + ->assertSee('Start') + ->assertSee('End') + ->assertDontSee('Configure') + ->assertDontSee('Build') + ->assertDontSee('Test') + ; + }); + }); + } + + public function testBuildTimelineStatusColors(): void + { + $startTime = Carbon::now()->subHours(2); + $endTime = Carbon::now()->subHour(); + + /** @var Build $build */ + $build = $this->project->builds()->create([ + 'siteid' => $this->site->id, + 'name' => Str::uuid()->toString(), + 'uuid' => Str::uuid()->toString(), + 'starttime' => $startTime->toIso8601String(), + 'endtime' => $endTime->toIso8601String(), + 'configureerrors' => 1, + 'configurewarnings' => 0, + 'builderrors' => 0, + 'buildwarnings' => 1, + 'testpassed' => 5, + 'testfailed' => 0, + 'testnotrun' => 0, + ]); + $build->configure()->create(Configure::factory()->make(['status' => 1])->toArray()); + + $this->browse(function (Browser $browser) use ($build): void { + $browser->resize(1920, 1080) + ->visit("/builds/{$build->id}") + ->waitFor('@build-timeline') + ->waitForText('Configure') + ->waitForText('Build') + ->waitForText('Test') + ->within('@build-timeline', function (Browser $timeline): void { + // Start (index 1) - Configure (index 2) - Build (index 3) - Test (index 4) - End (index 5) + $timeline->within('[data-test="timeline-step"]:nth-child(2)', function (Browser $step): void { + $step->assertSee('Configure') + ->assertAttributeContains('@step-icon', 'class', 'tw-text-error'); + }); + $timeline->within('[data-test="timeline-step"]:nth-child(3)', function (Browser $step): void { + $step->assertSee('Build') + ->assertAttributeContains('@step-icon', 'class', 'tw-text-warning'); + }); + $timeline->within('[data-test="timeline-step"]:nth-child(4)', function (Browser $step): void { + $step->assertSee('Test') + ->assertAttributeContains('@step-icon', 'class', 'tw-text-success'); + }); + }); + }); + } + + public function testBuildTimelineMobileLayout(): void + { + $startTime = Carbon::now()->subHours(2); + $endTime = Carbon::now()->subHour(); + + /** @var Build $build */ + $build = $this->project->builds()->create([ + 'siteid' => $this->site->id, + 'name' => Str::uuid()->toString(), + 'uuid' => Str::uuid()->toString(), + 'starttime' => $startTime->toIso8601String(), + 'endtime' => $endTime->toIso8601String(), + 'configureerrors' => 0, + 'configurewarnings' => 0, + 'configureduration' => 60, + ]); + $build->configure()->create(Configure::factory()->make()->toArray()); + + $this->browse(function (Browser $browser) use ($build): void { + $browser->resize(400, 800) + ->visit("/builds/{$build->id}") + ->waitFor('@build-timeline') + ->waitFor('@timeline-step-mobile') + ->waitForText('Configure') + ->within('[data-test="timeline-step-mobile"]:nth-child(2)', function (Browser $step): void { + $step->assertSee('Configure') + ->assertSee('1 min'); + }); + }); + } + + public function testBuildTimelineLinks(): void + { + $startTime = Carbon::now()->subHours(2); + $endTime = Carbon::now()->subHour(); + + /** @var Build $build */ + $build = $this->project->builds()->create([ + 'siteid' => $this->site->id, + 'name' => Str::uuid()->toString(), + 'uuid' => Str::uuid()->toString(), + 'starttime' => $startTime->toIso8601String(), + 'endtime' => $endTime->toIso8601String(), + 'configureerrors' => 0, + 'configurewarnings' => 0, + 'builderrors' => 0, + 'buildwarnings' => 0, + 'testpassed' => 5, + 'testfailed' => 0, + 'testnotrun' => 0, + ]); + $build->configure()->create(Configure::factory()->make()->toArray()); + + $this->browse(function (Browser $browser) use ($build): void { + $browser->resize(1920, 1080) + ->visit("/builds/{$build->id}") + ->waitFor('@build-timeline') + ->waitForText('Configure') + ->waitForText('Build') + ->waitForText('Test') + ->within('@build-timeline', function (Browser $timeline) use ($build): void { + $timeline->within('[data-test="timeline-step"]:nth-child(2)', function (Browser $step) use ($build): void { + $step->waitFor('@step-label') + ->assertAttributeContains('@step-label', 'href', "/builds/{$build->id}/configure"); + }); + $timeline->within('[data-test="timeline-step"]:nth-child(3)', function (Browser $step) use ($build): void { + $step->waitFor('@step-label') + ->assertAttributeContains('@step-label', 'href', "/builds/{$build->id}/build"); + }); + $timeline->within('[data-test="timeline-step"]:nth-child(4)', function (Browser $step) use ($build): void { + $step->waitFor('@step-label') + ->assertAttributeContains('@step-label', 'href', "/builds/{$build->id}/tests"); + }); + }); + }); + } + + public function testBuildTimelineZeroDuration(): void + { + $startTime = Carbon::now()->subHours(2); + $endTime = Carbon::now()->subHour(); + + /** @var Build $build */ + $build = $this->project->builds()->create([ + 'siteid' => $this->site->id, + 'name' => Str::uuid()->toString(), + 'uuid' => Str::uuid()->toString(), + 'starttime' => $startTime->toIso8601String(), + 'endtime' => $endTime->toIso8601String(), + 'configureerrors' => 0, + 'configurewarnings' => 0, + 'configureduration' => 0, + ]); + $build->configure()->create(Configure::factory()->make()->toArray()); + + $this->browse(function (Browser $browser) use ($build): void { + $browser->resize(1920, 1080) + ->visit("/builds/{$build->id}") + ->waitFor('@build-timeline') + ->waitForText('0 sec') + ->assertSeeIn('@build-timeline', '0 sec'); + }); + } }