Implementation UI warnings for outdated shipped feature (#4506)

* Implementation

* Fix comments, pending tests

* Finish tests

* Ignore old shipped feature
This commit is contained in:
Kyle Ju 2024-10-31 14:35:11 -07:00 коммит произвёл GitHub
Родитель 5c4949545d
Коммит 9a6b2f610d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
4 изменённых файлов: 312 добавлений и 29 удалений

Просмотреть файл

@ -114,6 +114,8 @@ export class ChromedashFeaturePage extends LitElement {
@state()
isUpcoming = false;
@state()
hasShipped = false;
@state()
currentDate: number = Date.now();
@state()
// The closest milestone shipping date as an ISO string.
@ -128,11 +130,29 @@ export class ChromedashFeaturePage extends LitElement {
return this.feature && Object.keys(this.feature).length !== 0;
}
async fetchClosestShippingDate(milestone: number): Promise<string> {
if (milestone === 0) {
return '';
}
try {
const newMilestonesInfo = await window.csClient.getSpecifiedChannels(
milestone,
milestone
);
return newMilestonesInfo[milestone]?.final_beta;
} catch {
showToastMessage(
'Some errors occurred. Please refresh the page or try again later.'
);
return '';
}
}
/**
* Determine if this feature is upcoming - scheduled to ship
* within two milestones, and find the closest shipping date
* for that milestone.*/
calcUpcoming(channels, stages: Array<StageDict>) {
* within two milestones, then find the closest shipping date
* for that upcoming milestone or an already shipped milestone.*/
async findClosestShippingDate(channels, stages: Array<StageDict>) {
const latestStableVersion = channels['stable']?.version;
if (!latestStableVersion || !stages) {
return;
@ -164,13 +184,76 @@ export class ChromedashFeaturePage extends LitElement {
this.closestShippingDate = channels[key].final_beta;
}
});
} else {
// If not upcoming, find the closest milestone that has shipped.
let latestMilestone = 0;
for (const ms of shippingMilestones) {
if (ms && ms <= latestStableVersion) {
latestMilestone = Math.max(latestMilestone, ms);
}
}
if (latestMilestone === latestStableVersion) {
this.closestShippingDate = channels['stable']?.final_beta;
this.hasShipped = true;
} else {
this.closestShippingDate =
await this.fetchClosestShippingDate(latestMilestone);
this.hasShipped = true;
}
}
}
/**
* Determine if it should show warnings to a feature author, if
* a shipped feature is outdated, and it has edit access.*/
isShippedFeatureOutdatedForAuthor() {
return this.userCanEdit() && this.isShippedFeatureOutdated();
}
/**
* Determine if it should show warnings to all readers, if
* a shipped feature is outdated, and last update was > 2 months.*/
isShippedFeatureOutdatedForAll() {
if (!this.isShippedFeatureOutdated()) {
return false;
}
// Represent two months grace period.
const nineWeekPeriod = 9 * 7 * 24 * 60 * 60 * 1000;
const isVerified = isVerifiedWithinGracePeriod(
this.feature.accurate_as_of,
this.currentDate,
nineWeekPeriod
);
return !isVerified;
}
/**
* A feature is outdated if it has shipped, and its
* accurate_as_of is before its latest shipping date before today.*/
isShippedFeatureOutdated(): boolean {
// Check if a feature has shipped.
if (!this.hasShipped) {
return false;
}
// If accurate_as_of is missing from a shipped feature, it is likely
// an old feature. Treat it as not oudated.
if (!this.feature.accurate_as_of) {
return false;
}
return (
Date.parse(this.feature.accurate_as_of) <
Date.parse(this.closestShippingDate)
);
}
/**
* A feature is outdated if it is scheduled to ship in the next 2 milestones,
* and its accurate_as_of date is at least 4 weeks ago.*/
isFeatureOutdated(): boolean {
isUpcomingFeatureOutdated(): boolean {
if (!this.isUpcoming) {
return false;
}
@ -214,7 +297,7 @@ export class ChromedashFeaturePage extends LitElement {
if (this.feature.name) {
document.title = `${this.feature.name} - ${this.appTitle}`;
}
this.calcUpcoming(channels, feature.stages);
this.findClosestShippingDate(channels, feature.stages);
this.loading = false;
}
)
@ -486,7 +569,7 @@ export class ChromedashFeaturePage extends LitElement {
</div>
`);
}
if (this.isFeatureOutdated()) {
if (this.isUpcomingFeatureOutdated()) {
if (this.userCanEdit()) {
warnings.push(html`
<div class="warning layout horizontal center">
@ -530,6 +613,59 @@ export class ChromedashFeaturePage extends LitElement {
`);
}
}
if (this.isShippedFeatureOutdated()) {
if (this.isShippedFeatureOutdatedForAuthor()) {
warnings.push(html`
<div class="warning layout horizontal center">
<span
class="tooltip"
id="shipped-outdated-author"
title="Feature outdated "
>
<iron-icon icon="chromestatus:error" data-tooltip></iron-icon>
</span>
<span>
Your feature hasn't been verified as accurate since&nbsp;
<sl-relative-time
date=${this.feature.accurate_as_of}
></sl-relative-time
>, but it claims to have shipped&nbsp;
<sl-relative-time
date=${this.closestShippingDate}
></sl-relative-time
>. Please
<a href="/guide/verify_accuracy/${this.featureId}"
>verify that your feature is accurate</a
>.
</span>
</div>
`);
} else if (this.isShippedFeatureOutdatedForAll()) {
warnings.push(html`
<div class="warning layout horizontal center">
<span
class="tooltip"
id="shipped-outdated-all"
title="Feature outdated "
>
<iron-icon icon="chromestatus:error" data-tooltip></iron-icon>
</span>
<span>
This feature hasn't been verified as accurate since&nbsp;
<sl-relative-time
date=${this.feature.accurate_as_of}
></sl-relative-time
>, but it claims to have shipped&nbsp;
<sl-relative-time
date=${this.closestShippingDate}
></sl-relative-time
>.
</span>
</div>
`);
}
}
return warnings;
}

Просмотреть файл

@ -65,6 +65,12 @@ describe('chromedash-feature-page', () => {
version: 79,
earliest_beta: '2020-02-13T00:00:00',
mstone: 'fake milestone number',
final_beta: '2020-03-13T00:00:00',
},
20: {
version: 20,
final_beta: '2018-02-13T00:00:00',
mstone: 'fake milestone number',
},
};
const channelsPromise = Promise.resolve(channels);
@ -148,11 +154,13 @@ describe('chromedash-feature-page', () => {
sinon.stub(window.csClient, 'getFeatureProcess');
sinon.stub(window.csClient, 'getStars');
sinon.stub(window.csClient, 'getFeatureProgress');
sinon.stub(window.csClient, 'getSpecifiedChannels');
window.csClient.getGates.returns(gatesPromise);
window.csClient.getComments.returns(commentsPromise);
window.csClient.getFeatureProcess.returns(processPromise);
window.csClient.getStars.returns(starsPromise);
window.csClient.getFeatureProgress.returns(progressPromise);
window.csClient.getSpecifiedChannels.returns(channelsPromise);
// For the child component - chromedash-gantt
sinon.stub(window.csClient, 'getChannels');
@ -164,6 +172,7 @@ describe('chromedash-feature-page', () => {
window.csClient.getFeatureProcess.restore();
window.csClient.getStars.restore();
window.csClient.getChannels.restore();
window.csClient.getSpecifiedChannels.restore();
});
it('renders with no data', async () => {
@ -323,7 +332,7 @@ describe('chromedash-feature-page', () => {
assert.include(consensusSection.innerHTML, 'fake webdev view text');
});
it('calcUpcoming() tests', async () => {
it('findClosestShippingDate() tests for isUpcoming state', async () => {
const featureId = 123456;
const contextLink = '/features';
const feature: any = structuredClone(await validFeaturePromise);
@ -338,34 +347,107 @@ describe('chromedash-feature-page', () => {
);
assert.exists(component);
component.calcUpcoming({}, feature.stages);
component.findClosestShippingDate({}, feature.stages);
assert.isFalse(component.isUpcoming);
assert.equal(component.closestShippingDate, '');
component.calcUpcoming(channels, []);
component.findClosestShippingDate(channels, []);
assert.isFalse(component.isUpcoming);
assert.equal(component.closestShippingDate, '');
// No shipping milestones.
let stages: any = structuredClone(feature.stages);
stages[2].stage_type = 130;
component.calcUpcoming(channels, stages);
component.findClosestShippingDate(channels, stages);
assert.isFalse(component.isUpcoming);
assert.equal(component.closestShippingDate, '');
// No upcoming shipping milestones.
stages = structuredClone(feature.stages);
stages[2].desktop_first = 20;
component.calcUpcoming(channels, stages);
component.findClosestShippingDate(channels, stages);
assert.isFalse(component.isUpcoming);
assert.isFalse(component.hasShipped);
assert.equal(component.closestShippingDate, '');
component.calcUpcoming(channels, feature.stages);
component.findClosestShippingDate(channels, feature.stages);
assert.isTrue(component.isUpcoming);
assert.isFalse(component.hasShipped);
assert.equal(component.closestShippingDate, '2020-03-13T00:00:00');
});
it('isFeatureOutdated() tests', async () => {
it('findClosestShippingDate() tests for hasShipped state', async () => {
const featureId = 123456;
const contextLink = '/features';
const feature: any = structuredClone(await validFeaturePromise);
const component: ChromedashFeaturePage =
await fixture<ChromedashFeaturePage>(
html`<chromedash-feature-page
.user=${user}
.featureId=${featureId}
.contextLink=${contextLink}
>
</chromedash-feature-page>`
);
assert.exists(component);
component.findClosestShippingDate({}, feature.stages);
assert.isFalse(component.hasShipped);
assert.equal(component.closestShippingDate, '');
component.findClosestShippingDate(channels, []);
assert.isFalse(component.hasShipped);
assert.equal(component.closestShippingDate, '');
// No shipping milestones.
let stages: any = structuredClone(feature.stages);
stages[2].stage_type = 130;
component.findClosestShippingDate(channels, stages);
assert.isFalse(component.hasShipped);
assert.equal(component.closestShippingDate, '');
// No shipped milestones in the past.
const testChannels: any = structuredClone(channels);
testChannels['stable'].version = 10;
component.findClosestShippingDate(testChannels, stages);
assert.isFalse(component.hasShipped);
assert.equal(component.closestShippingDate, '');
// Shipped on the stable milestone.
stages = structuredClone(feature.stages);
stages[2].desktop_first = 79;
component.findClosestShippingDate(channels, stages);
assert.isFalse(component.isUpcoming);
assert.isTrue(component.hasShipped);
assert.equal(component.closestShippingDate, '2020-03-13T00:00:00');
});
it('findClosestShippingDate() tests when fetch specific channels', async () => {
const featureId = 123456;
const contextLink = '/features';
const feature: any = structuredClone(await validFeaturePromise);
feature.stages[2].desktop_first = 20;
window.csClient.getFeature
.withArgs(featureId)
.returns(Promise.resolve(feature));
const component: ChromedashFeaturePage =
await fixture<ChromedashFeaturePage>(
html`<chromedash-feature-page
.user=${user}
.featureId=${featureId}
.contextLink=${contextLink}
>
</chromedash-feature-page>`
);
assert.exists(component);
assert.isFalse(component.isUpcoming);
assert.isTrue(component.hasShipped);
assert.equal(component.closestShippingDate, '2018-02-13T00:00:00');
});
it('isUpcomingFeatureOutdated() tests', async () => {
const featureId = 123456;
const contextLink = '/features';
const feature: any = structuredClone(await validFeaturePromise);
@ -385,14 +467,14 @@ describe('chromedash-feature-page', () => {
component.currentDate = new Date('2024-10-23').getTime();
assert.exists(component);
component.calcUpcoming(channels, feature.stages);
component.findClosestShippingDate(channels, feature.stages);
assert.isTrue(component.isUpcoming);
assert.equal(component.closestShippingDate, '2020-03-13T00:00:00');
assert.isTrue(component.isFeatureOutdated());
assert.isTrue(component.isUpcomingFeatureOutdated());
// accurate_as_of is not outdated and within the 4-week grace period.
component.currentDate = new Date('2024-09-18').getTime();
assert.isFalse(component.isFeatureOutdated());
assert.isFalse(component.isUpcomingFeatureOutdated());
});
it('render the oudated warning when outdated', async () => {
@ -415,8 +497,74 @@ describe('chromedash-feature-page', () => {
component.currentDate = new Date('2024-10-23').getTime();
assert.exists(component);
component.calcUpcoming(channels, feature.stages);
component.findClosestShippingDate(channels, feature.stages);
const oudated = component.shadowRoot!.querySelector('#outdated-icon');
assert.exists(oudated);
});
it('render shipped feature outdated warnings for authors', async () => {
const featureId = 123456;
const contextLink = '/features';
const feature: any = structuredClone(await validFeaturePromise);
feature.accurate_as_of = '2017-10-28 21:51:34.22386';
feature.stages[2].desktop_first = 20;
window.csClient.getFeature
.withArgs(featureId)
.returns(Promise.resolve(feature));
const component: ChromedashFeaturePage =
await fixture<ChromedashFeaturePage>(
html`<chromedash-feature-page
.user=${user}
.featureId=${featureId}
.contextLink=${contextLink}
>
</chromedash-feature-page>`
);
component.currentDate = new Date('2017-10-29').getTime();
await component.updateComplete;
assert.exists(component);
assert.isTrue(component.isShippedFeatureOutdated());
assert.isTrue(component.isShippedFeatureOutdatedForAuthor());
assert.isFalse(component.isShippedFeatureOutdatedForAll());
const oudated = component.shadowRoot!.querySelector(
'#shipped-outdated-author'
);
assert.exists(oudated);
});
it('render shipped feature outdated warnings for all', async () => {
const featureId = 123456;
const contextLink = '/features';
const feature: any = structuredClone(await validFeaturePromise);
feature.accurate_as_of = '2017-10-28 21:51:34.22386';
feature.stages[2].desktop_first = 20;
window.csClient.getFeature
.withArgs(featureId)
.returns(Promise.resolve(feature));
const component: ChromedashFeaturePage =
await fixture<ChromedashFeaturePage>(
html`<chromedash-feature-page
.featureId=${featureId}
.contextLink=${contextLink}
>
</chromedash-feature-page>`
);
component.currentDate = new Date('2020-10-29').getTime();
await component.updateComplete;
assert.exists(component);
assert.isTrue(component.isShippedFeatureOutdated());
// undefined because this.user is undefined.
assert.isUndefined(component.isShippedFeatureOutdatedForAuthor());
assert.isTrue(component.isShippedFeatureOutdatedForAll());
const oudated = component.shadowRoot!.querySelector(
'#shipped-outdated-all'
);
assert.exists(oudated);
});
});

Просмотреть файл

@ -4,6 +4,7 @@ import {customElement, property, state} from 'lit/decorators.js';
import {createRef, ref} from 'lit/directives/ref.js';
import {ROADMAP_MILESTONE_CARD_CSS} from '../css/elements/chromedash-roadmap-milestone-card-css.js';
import {Channels, ReleaseInfo} from '../js-src/cs-client.js';
import {isVerifiedWithinGracePeriod} from './utils.js';
const REMOVED_STATUS = ['Removed'];
const DEPRECATED_STATUS = ['Deprecated', 'No longer pursuing'];
@ -261,17 +262,12 @@ export class ChromedashRoadmapMilestoneCard extends LitElement {
) {
return false;
}
if (!accurateAsOf) {
return true;
}
const accurateDate = Date.parse(accurateAsOf);
// 4-week period.
const gracePeriod = 4 * 7 * 24 * 60 * 60 * 1000;
if (accurateDate + gracePeriod < this.currentDate) {
return true;
}
return false;
const isVerified = isVerifiedWithinGracePeriod(
accurateAsOf,
this.currentDate
);
return !isVerified;
}
_cardFeatureItemTemplate(f, shippingType) {

Просмотреть файл

@ -663,17 +663,20 @@ export function extensionMilestoneIsValid(value, currentMilestone) {
*
* @param accurateAsOf The accurate_as_of date as an ISO string.
* @param currentDate The current date in milliseconds.
* @param gracePeriod The grace period in milliseconds. Defaults
* to ACCURACY_GRACE_PERIOD.
*/
export function isVerifiedWithinGracePeriod(
accurateAsOf: string | undefined,
currentDate: number
currentDate: number,
gracePeriod: number = ACCURACY_GRACE_PERIOD
) {
if (!accurateAsOf) {
return false;
}
const accurateDate = Date.parse(accurateAsOf);
if (accurateDate + ACCURACY_GRACE_PERIOD < currentDate) {
if (accurateDate + gracePeriod < currentDate) {
return false;
}