Implementation UI warnings for outdated shipped feature (#4506)
* Implementation * Fix comments, pending tests * Finish tests * Ignore old shipped feature
This commit is contained in:
Родитель
5c4949545d
Коммит
9a6b2f610d
|
@ -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
|
||||
<sl-relative-time
|
||||
date=${this.feature.accurate_as_of}
|
||||
></sl-relative-time
|
||||
>, but it claims to have shipped
|
||||
<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
|
||||
<sl-relative-time
|
||||
date=${this.feature.accurate_as_of}
|
||||
></sl-relative-time
|
||||
>, but it claims to have shipped
|
||||
<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;
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче