diff --git a/README.md b/README.md
index 8f812dd05795..e1375895b023 100644
--- a/README.md
+++ b/README.md
@@ -77,14 +77,20 @@ To run the Playwright visual tests (aka end-to-end tests), the command to use is
npm run pwtests --workspace=playwright
```
-If there are errors, or if you need to update any of the screeenshot images,
-you can update all images for all tests with:
+If there are errors, they will be displayed in the console.
+If you need to update any of the screenshot images, you will see differences in
+the `packages/playwright/test-results` directory, and if they look correct,
+then you can update _all_ images for all tests with:
```bash
npm run pwtests-update --workspace=playwright
```
-Then check the `packages/playwright/test-results` directory for any changes.
+The updated images are also added to the __screenshots__ directory.
+If you change test file names, or test names, or screenshot image names, then
+you can manually delete the old screenshots, or simply delete all and update all.
+There is no way to run just one test or test file yet, but playwright supports
+doing that, so we should be able to add a parameter to the test running commands.
There is some additional information for developers in developer-documentation.md.
@@ -124,7 +130,7 @@ of globals for debugging and running the site locally.
### Deploying
-If you have uncommited local changes, the appengine version name will end with `-tainted`.
+If you have uncommitted local changes, the appengine version name will end with `-tainted`.
It is OK to test on staging with tainted versions, but everything should be committed
(and thus not tainted) before staging a version that can later be pushed to prod.
diff --git a/api/api_specs.py b/api/api_specs.py
index 3ae322d9d400..6d08dab0e4ae 100644
--- a/api/api_specs.py
+++ b/api/api_specs.py
@@ -94,6 +94,7 @@
STAGE_FIELD_DATA_TYPES: FIELD_INFO_DATA_TYPE = [
('announcement_url', 'link'),
('browser', 'str'),
+ ('ot_description', 'str'),
('display_name', 'str'),
('enterprise_policies', 'split_str'),
('finch_url', 'link'),
@@ -104,10 +105,15 @@
('origin_trial_feedback_url', 'link'),
('origin_trial_id', 'str'),
('ot_chromium_trial_name', 'str'),
+ ('ot_action_requested', 'bool'),
('ot_documentation_url', 'link'),
+ ('ot_emails', 'emails'),
+ ('ot_feedback_submission_url', 'link'),
('ot_has_third_party_support', 'bool'),
('ot_is_critical_trial', 'bool'),
('ot_is_deprecation_trial', 'bool'),
+ ('ot_owner_email', 'str'),
+ ('ot_request_note', 'str'),
('ot_webfeature_use_counter', 'str'),
('rollout_impact', 'int'),
('rollout_milestone', 'int'),
diff --git a/api/comments_api.py b/api/comments_api.py
index d3ec9fa7ac22..f8d13b2a6e83 100644
--- a/api/comments_api.py
+++ b/api/comments_api.py
@@ -61,10 +61,8 @@ def do_get(self, **kwargs) -> dict[str, list[dict[str, Any]]]:
"""Return a list of all review comments on the given feature."""
feature_id = kwargs['feature_id']
gate_id = kwargs.get('gate_id', None)
- comments_only = self.get_bool_arg('comments_only')
# Note: We assume that anyone may view approval comments.
- comments = Activity.get_activities(
- feature_id, gate_id, comments_only=comments_only)
+ comments = Activity.get_activities(feature_id, gate_id)
user = self.get_current_user()
is_admin = permissions.can_admin_site(user)
diff --git a/api/comments_api_test.py b/api/comments_api_test.py
index c61315594e27..a86d1e9ecfc9 100644
--- a/api/comments_api_test.py
+++ b/api/comments_api_test.py
@@ -120,21 +120,20 @@ def test_get__empty(self):
self.assertEqual({'comments': []}, actual_response)
def test_get__legacy_comments(self):
- """We can get legacy comments."""
+ """We no longer return legacy gate comments when gate_id is specified."""
testing_config.sign_out()
testing_config.sign_in('user7@example.com', 123567890)
legacy_comment = Activity(
feature_id=self.feature_id, author='owner1@example.com',
- created=NOW, content='nothing')
+ created=NOW, content='nothing') # no gate_id
legacy_comment.put()
with test_app.test_request_context(self.request_path):
actual_response = self.handler.do_get(
feature_id=self.feature_id, gate_id=self.gate_1_id)
testing_config.sign_out()
- actual_comment = actual_response['comments'][0]
- self.assertEqual('nothing', actual_comment['content'])
+ self.assertEqual([], actual_response['comments'])
def test_get__all_some(self):
"""We can get all comments for a given approval."""
diff --git a/api/converters.py b/api/converters.py
index dad9bd97b336..d02dc15988b2 100644
--- a/api/converters.py
+++ b/api/converters.py
@@ -177,6 +177,7 @@ def stage_to_json_dict(
'created': str(stage.created),
'feature_id': stage.feature_id,
'stage_type': stage.stage_type,
+ 'ot_description': stage.ot_description,
'display_name': stage.display_name,
'intent_stage': INTENT_STAGES_BY_STAGE_TYPE.get(
stage.stage_type, INTENT_NONE),
@@ -191,11 +192,17 @@ def stage_to_json_dict(
'experiment_risks': stage.experiment_risks,
'origin_trial_id': stage.origin_trial_id,
'origin_trial_feedback_url': stage.origin_trial_feedback_url,
+ 'ot_action_requested': stage.ot_action_requested,
'ot_chromium_trial_name': stage.ot_chromium_trial_name,
+ 'ot_description': stage.ot_description,
+ 'ot_display_name': stage.ot_display_name,
'ot_documentation_url': stage.ot_documentation_url,
+ 'ot_emails': stage.ot_emails,
+ 'ot_feedback_submission_url': stage.ot_feedback_submission_url,
'ot_has_third_party_support': stage.ot_has_third_party_support,
'ot_is_critical_trial': stage.ot_is_critical_trial,
'ot_is_deprecation_trial': stage.ot_is_deprecation_trial,
+ 'ot_owner_email': stage.ot_owner_email,
'ot_webfeature_use_counter': stage.ot_webfeature_use_counter,
'extensions': [],
'experiment_extension_reason': stage.experiment_extension_reason,
diff --git a/api/features_api.py b/api/features_api.py
index 9d58671170c7..7756ed78c0cb 100644
--- a/api/features_api.py
+++ b/api/features_api.py
@@ -144,6 +144,16 @@ def do_search(self):
'total_count': total_count,
}
+ # Query-string parameter 'releaseNotesMilestone' is provided
+ release_notes_milestone = self.get_int_arg('releaseNotesMilestone')
+ if release_notes_milestone:
+ features_in_release_notes = feature_helpers.get_features_in_release_notes(
+ milestone=release_notes_milestone)
+ return {
+ 'features': features_in_release_notes,
+ 'total_count': len(features_in_release_notes)
+ }
+
user_query = self.request.args.get('q', '')
sort_spec = self.request.args.get('sort')
num = self.get_int_arg('num', search.DEFAULT_RESULTS_PER_PAGE)
diff --git a/api/reviews_api.py b/api/reviews_api.py
index f728ea6d8ba9..67190b74f9a9 100644
--- a/api/reviews_api.py
+++ b/api/reviews_api.py
@@ -52,7 +52,9 @@ def do_post(self, **kwargs) -> dict[str, str]:
if gate.feature_id != feature_id:
self.abort(400, msg='Mismatched feature and gate')
- old_state = gate.state
+ old_votes = Vote.get_votes(
+ feature_id=feature_id, gate_id=gate_id, set_by=user.email())
+ old_state = old_votes[0].state if old_votes else Vote.NO_RESPONSE
self.require_permissions(user, feature, gate, new_state)
# Note: We no longer write Approval entities.
@@ -60,7 +62,8 @@ def do_post(self, **kwargs) -> dict[str, str]:
user.email(), gate_id)
if new_state == Vote.REVIEW_REQUESTED:
- notifier_helpers.notify_approvers_of_reviews(feature, gate)
+ notifier_helpers.notify_approvers_of_reviews(
+ feature, gate, user.email())
else:
notifier_helpers.notify_subscribers_of_vote_changes(
feature, gate, user.email(), new_state, old_state)
diff --git a/api/reviews_api_test.py b/api/reviews_api_test.py
index a76fc68500b8..02a21732e0ea 100644
--- a/api/reviews_api_test.py
+++ b/api/reviews_api_test.py
@@ -229,8 +229,9 @@ def test_post__add_new_vote(self, mock_get_approvers, mock_notifier):
self.assertEqual(vote.set_by, 'reviewer1@example.com')
self.assertEqual(vote.state, Vote.NEEDS_WORK)
- mock_notifier.assert_called_once_with(self.feature_1,
- self.gate_1, 'reviewer1@example.com', Vote.NEEDS_WORK, Vote.NA)
+ mock_notifier.assert_called_once_with(
+ self.feature_1, self.gate_1, 'reviewer1@example.com',
+ Vote.NEEDS_WORK, Vote.NO_RESPONSE)
@mock.patch('internals.notifier_helpers.notify_subscribers_of_vote_changes')
@mock.patch('internals.approval_defs.get_approvers')
@@ -254,8 +255,9 @@ def test_post__update_vote(self, mock_get_approvers, mock_notifier):
self.assertEqual(vote.set_by, 'reviewer1@example.com')
self.assertEqual(vote.state, Vote.DENIED)
- mock_notifier.assert_called_once_with(self.feature_1,
- self.gate_1, 'reviewer1@example.com', Vote.DENIED, Vote.NA)
+ mock_notifier.assert_called_once_with(
+ self.feature_1, self.gate_1, 'reviewer1@example.com',
+ Vote.DENIED, Vote.APPROVED)
@mock.patch('internals.notifier_helpers.notify_approvers_of_reviews')
@mock.patch('internals.approval_defs.get_approvers')
@@ -278,7 +280,8 @@ def test_post__request_review(self, mock_get_approvers, mock_notifier):
self.assertEqual(vote.set_by, 'owner1@example.com')
self.assertEqual(vote.state, Vote.REVIEW_REQUESTED)
- mock_notifier.assert_called_once_with(self.feature_1, self.gate_1)
+ mock_notifier.assert_called_once_with(
+ self.feature_1, self.gate_1, 'owner1@example.com')
class GatesAPITest(testing_config.CustomTestCase):
diff --git a/api/stages_api_test.py b/api/stages_api_test.py
index 4f2ef774ce51..4733bbbe5fa2 100644
--- a/api/stages_api_test.py
+++ b/api/stages_api_test.py
@@ -67,11 +67,20 @@ def setUp(self):
origin_trial_id='-5269211564023480319',
ux_emails=['ux_person@example.com'],
intent_thread_url='https://example.com/intent',
+ ot_action_requested=True,
ot_chromium_trial_name='ExampleChromiumTrialName',
+ ot_description='An origin trial\'s description',
+ ot_display_name='The Origin Trial Display Name',
ot_documentation_url='https://example.com/ot_docs',
+ ot_emails=['user1@example.com', 'user2@example.com'],
+ ot_feedback_submission_url='https://example.com/submit_feedback',
ot_has_third_party_support=True,
ot_is_deprecation_trial=True,
ot_is_critical_trial=True,
+ # ot_request_note should remain confidential and not be displayed in
+ # requests obtaining information about the stage.
+ ot_request_note='Additional info',
+ ot_owner_email='ot_owner@example.com',
ot_webfeature_use_counter='kExampleUseCounter',
milestones=MilestoneSet(desktop_first=100),
experiment_goals='To be the very best.',
@@ -98,11 +107,17 @@ def setUp(self):
'enterprise_policies': [],
'origin_trial_id': None,
'origin_trial_feedback_url': None,
+ 'ot_action_requested': False,
'ot_chromium_trial_name': None,
+ 'ot_description': None,
+ 'ot_display_name': None,
'ot_documentation_url': None,
+ 'ot_emails': [],
+ 'ot_feedback_submission_url': None,
'ot_has_third_party_support': False,
'ot_is_critical_trial': False,
'ot_is_deprecation_trial': False,
+ 'ot_owner_email': None,
'ot_webfeature_use_counter': None,
'experiment_extension_reason': None,
'experiment_goals': 'To be the very best.',
@@ -188,11 +203,17 @@ def test_get__valid_with_extension(self):
'experiment_goals': 'To be the very best.',
'experiment_risks': None,
'origin_trial_feedback_url': None,
+ 'ot_action_requested': False,
'ot_chromium_trial_name': None,
+ 'ot_description': None,
+ 'ot_display_name': None,
'ot_documentation_url': None,
+ 'ot_emails': [],
+ 'ot_feedback_submission_url': None,
'ot_has_third_party_support': False,
'ot_is_critical_trial': False,
'ot_is_deprecation_trial': False,
+ 'ot_owner_email': None,
'ot_webfeature_use_counter': None,
'announcement_url': None,
'enterprise_policies': [],
@@ -235,11 +256,17 @@ def test_get__valid_with_extension(self):
'enterprise_policies': [],
'origin_trial_id': '-5269211564023480319',
'origin_trial_feedback_url': None,
+ 'ot_action_requested': True,
'ot_chromium_trial_name': 'ExampleChromiumTrialName',
+ 'ot_description': 'An origin trial\'s description',
+ 'ot_display_name': 'The Origin Trial Display Name',
'ot_documentation_url': 'https://example.com/ot_docs',
+ 'ot_emails': ['user1@example.com', 'user2@example.com'],
+ 'ot_feedback_submission_url': 'https://example.com/submit_feedback',
'ot_has_third_party_support': True,
'ot_is_critical_trial': True,
'ot_is_deprecation_trial': True,
+ 'ot_owner_email': 'ot_owner@example.com',
'ot_webfeature_use_counter': 'kExampleUseCounter',
'rollout_details': None,
'rollout_impact': 2,
diff --git a/client-src/components.js b/client-src/components.js
index d5afa7705030..72a6a5908a56 100644
--- a/client-src/components.js
+++ b/client-src/components.js
@@ -81,6 +81,7 @@ import './elements/chromedash-legend';
import './elements/chromedash-login-required-page';
import './elements/chromedash-metadata';
import './elements/chromedash-myfeatures-page';
+import './elements/chromedash-ot-creation-page';
import './elements/chromedash-preflight-dialog';
import './elements/chromedash-process-overview';
import './elements/chromedash-settings-page';
diff --git a/client-src/elements/chromedash-activity-page.js b/client-src/elements/chromedash-activity-page.js
index 4debe4ca9e92..478b35ae21fe 100644
--- a/client-src/elements/chromedash-activity-page.js
+++ b/client-src/elements/chromedash-activity-page.js
@@ -70,7 +70,7 @@ export class ChromedashActivityPage extends LitElement {
fetchComments() {
this.loading = true;
Promise.all([
- window.csClient.getComments(this.featureId, null, false),
+ window.csClient.getComments(this.featureId, null),
]).then(([commentRes]) => {
this.comments = commentRes.comments;
this.needsPost = false;
@@ -85,7 +85,7 @@ export class ChromedashActivityPage extends LitElement {
commentArea.value = '';
this.needsPost = false;
Promise.all([
- window.csClient.getComments(this.featureId, null, false),
+ window.csClient.getComments(this.featureId, null),
]).then(([commentRes]) => {
this.comments = commentRes.comments;
}).catch(() => {
diff --git a/client-src/elements/chromedash-app.js b/client-src/elements/chromedash-app.js
index 5cbd5fe3cbcc..163fc27a3521 100644
--- a/client-src/elements/chromedash-app.js
+++ b/client-src/elements/chromedash-app.js
@@ -372,6 +372,15 @@ class ChromedashApp extends LitElement {
this.currentPage = ctx.path;
this.hideSidebar();
});
+ page('/ot_creation_request/:featureId(\\d+)/:stageId(\\d+)', (ctx) => {
+ if (!this.setupNewPage(ctx, 'chromedash-ot-creation-page')) return;
+ this.pageComponent.featureId = parseInt(ctx.params.featureId);
+ this.pageComponent.stageId = parseInt(ctx.params.stageId);
+ this.pageComponent.nextPage = this.currentPage;
+ this.pageComponent.appTitle = this.appTitle;
+ this.currentPage = ctx.path;
+ this.hideSidebar();
+ });
page('/guide/stage/:featureId(\\d+)/metadata', (ctx) => {
if (!this.setupNewPage(ctx, 'chromedash-guide-metadata-page')) return;
this.pageComponent.featureId = parseInt(ctx.params.featureId);
diff --git a/client-src/elements/chromedash-drawer.js b/client-src/elements/chromedash-drawer.js
index fa81b2282d8f..f2b9707d000c 100644
--- a/client-src/elements/chromedash-drawer.js
+++ b/client-src/elements/chromedash-drawer.js
@@ -120,6 +120,7 @@ export class ChromedashDrawer extends LitElement {
connectedCallback() {
super.connectedCallback();
+
// user is passed in from chromedash-app
if (this.user && this.user.email) return;
@@ -134,6 +135,8 @@ export class ChromedashDrawer extends LitElement {
this.initializeGoogleSignIn();
}
if (this.devMode == 'True') {
+ // Insert the testing signin second, so it appears to the left
+ // of the google signin button, with a large margin on the right.
this.initializeTestingSignIn();
}
}
@@ -160,36 +163,36 @@ export class ChromedashDrawer extends LitElement {
}
initializeTestingSignIn() {
- if (this.devMode != 'True') {
- return;
- }
-
// Create DEV_MODE login button for testing
const signInTestingButton = document.createElement('button');
signInTestingButton.innerText = 'Sign in as example@chromium.org';
+ signInTestingButton.setAttribute('type', 'button');
signInTestingButton.setAttribute('data-testid', 'dev-mode-sign-in-button');
+ signInTestingButton.setAttribute('style',
+ 'position:fixed; right:0; z-index:1000; background: lightblue; border: 1px solid blue;');
+
signInTestingButton.addEventListener('click', () => {
// POST to '/dev/mock_login' to login as example@chromium.
- fetch('/dev/mock_login', {method: 'POST'}).then((response) => {
- if (!response.ok) {
- signInTestingButton.style.color = 'red';
- throw new Error('Sign in failed! Response:', response);
- }
- // Reload the page to display with the logged in user.
- const url = window.location.href.split('?')[0];
- window.history.replaceState(null, null, url);
- window.location = url;
- })
+ fetch('/dev/mock_login', {method: 'POST'})
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error('Sign in failed! Response:', response);
+ }
+ })
+ .then(() => {
+ setTimeout(() => {
+ const url = window.location.href.split('?')[0];
+ window.location = url;
+ }, 1000);
+ })
.catch((error) => {
console.error('Sign in failed.', error);
});
});
- const appComponent = document.querySelector('chromedash-app');
- if (appComponent) {
- appComponent.insertAdjacentElement('afterbegin', signInTestingButton); // for SPA
- } else {
- this.insertAdjacentElement('afterbegin', signInTestingButton); // for MPA
+ const signInButtonContainer = document.querySelector('body');
+ if (signInButtonContainer) {
+ signInButtonContainer.insertAdjacentElement('afterbegin', signInTestingButton); // for SPA
}
}
@@ -267,7 +270,7 @@ export class ChromedashDrawer extends LitElement {
return html`
Settings
- Sign out
+ Sign out
${this.user.can_create_feature && !this.isCurrentPage('/guide/new') ? html`
diff --git a/client-src/elements/chromedash-enterprise-release-notes-page.js b/client-src/elements/chromedash-enterprise-release-notes-page.js
index 1130ac1fe4fd..0af66fb91371 100644
--- a/client-src/elements/chromedash-enterprise-release-notes-page.js
+++ b/client-src/elements/chromedash-enterprise-release-notes-page.js
@@ -179,54 +179,31 @@ export class ChromedashEnterpriseReleaseNotesPage extends LitElement {
}));
}
- connectedCallback() {
- super.connectedCallback();
- Promise.all([
- window.csClient.getChannels(),
- window.csClient.searchFeatures(
- 'feature_type="New Feature or removal affecting enterprises" OR breaking_change=true'),
- ]).then(([channels, {features}]) => {
- this.channels = channels;
- const queryParams = parseRawQuery(window.location.search);
- if (milestoneQueryParamKey in queryParams) {
- this.selectedMilestone = parseInt(queryParams[milestoneQueryParamKey], 10);
- } else {
- this.selectedMilestone = this.channels.stable.version;
- updateURLParams(milestoneQueryParamKey, this.selectedMilestone);
- }
+ updateFeatures(features) {
+ // Simulate rollout stage for features with breaking changes and planned
+ // milestones but without rollout stages so that they appear on the release
+ // notes.
+ const featuresRequiringRolloutStages = features
+ .filter(({stages}) => !stages
+ .some(s => s.stage_type === STAGE_ENT_ROLLOUT) &&
+ stages.some(s => STAGE_TYPES_SHIPPING.has(s.stage_type)))
+ .map(f => ({
+ ...f,
+ stages: f.stages
+ .filter(s => STAGE_TYPES_SHIPPING.has(s.stage_type))
+ .map(this.convertShippingStageToRolloutStages).flatMap(x => x),
+ }));
- // Simulate rollout stage for features with breaking changes and planned
- // milestones but without rollout stages so that they appear on the release
- // notes.
- const featuresRequiringRolloutStages = features
- .filter(({stages}) => !stages
- .some(s => s.stage_type === STAGE_ENT_ROLLOUT) &&
- stages.some(s => STAGE_TYPES_SHIPPING.has(s.stage_type)))
- .map(f => ({
- ...f,
- stages: f.stages
- .filter(s => STAGE_TYPES_SHIPPING.has(s.stage_type))
- .map(this.convertShippingStageToRolloutStages).flatMap(x => x),
- }));
-
- // Filter out features that don't have rollout stages.
- // Ensure that the stages are only rollout stages.
- this.features = [...features, ...featuresRequiringRolloutStages]
- .filter(({stages}) => stages.some(s => s.stage_type === STAGE_ENT_ROLLOUT))
- .map(f => ({
- ...f,
- stages: f.stages
- .filter(s => s.stage_type === STAGE_ENT_ROLLOUT && !!s.rollout_milestone)
- .sort((a, b) => a.rollout_milestone - b.rollout_milestone)}));
- }).catch(() => {
- showToastMessage('Some errors occurred. Please refresh the page or try again later.');
- });
- }
+ // Filter out features that don't have rollout stages.
+ // Ensure that the stages are only rollout stages.
+ this.features = [...features, ...featuresRequiringRolloutStages]
+ .filter(({stages}) => stages.some(s => s.stage_type === STAGE_ENT_ROLLOUT))
+ .map(f => ({
+ ...f,
+ stages: f.stages
+ .filter(s => s.stage_type === STAGE_ENT_ROLLOUT && !!s.rollout_milestone)
+ .sort((a, b) => a.rollout_milestone - b.rollout_milestone)}));
- /**
- * Updates currentFeatures and upcomingFeatures based on the selected milestone.
- */
- updateCurrentAndUpcomingFeatures() {
// Features with a rollout stage in the selected milestone sorted with the highest impact.
this.currentFeatures = this.features
.filter(({stages}) => stages.some(s => s.rollout_milestone === this.selectedMilestone))
@@ -258,12 +235,35 @@ export class ChromedashEnterpriseReleaseNotesPage extends LitElement {
});
}
+ connectedCallback() {
+ window.csClient.getChannels()
+ .then(channels => {
+ this.channels = channels;
+ const queryParams = parseRawQuery(window.location.search);
+ if (milestoneQueryParamKey in queryParams) {
+ this.selectedMilestone = parseInt(queryParams[milestoneQueryParamKey], 10);
+ } else {
+ this.selectedMilestone = this.channels.stable.version;
+ updateURLParams(milestoneQueryParamKey, this.selectedMilestone);
+ }
+ })
+ .then(() => window.csClient.getFeaturesForEnterpriseReleaseNotes(this.selectedMilestone))
+ .then(({features}) => this.updateFeatures(features))
+ .catch(() => {
+ showToastMessage('Some errors occurred. Please refresh the page or try again later.');
+ })
+ .finally(() => super.connectedCallback());
+ }
+
updateSelectedMilestone() {
this.selectedMilestone = parseInt(this.shadowRoot.querySelector('#milestone-selector').value);
+ window.csClient.getFeaturesForEnterpriseReleaseNotes(this.selectedMilestone)
+ .then(({features}) => this.updateFeatures(features)).catch(() => {
+ showToastMessage('Some errors occurred. Please refresh the page or try again later.');
+ });
}
update() {
- this.updateCurrentAndUpcomingFeatures();
if (this.selectedMilestone !== undefined) {
updateURLParams(milestoneQueryParamKey, this.selectedMilestone);
}
@@ -392,7 +392,8 @@ export class ChromedashEnterpriseReleaseNotesPage extends LitElement {
m => m === this.selectedMilestone)}
${this.renderReleaseNotesDetailsSection(
`Upcoming Chrome browser updates`, this.upcomingFeatures,
- (m, milestones) => milestones.find(x => x> this.selectedMilestone) === m)}`;
+ (m, milestones) => milestones
+ .find(x => parseInt(x) > parseInt(this.selectedMilestone)) === m)}`;
}
render() {
diff --git a/client-src/elements/chromedash-enterprise-release-notes-page_test.js b/client-src/elements/chromedash-enterprise-release-notes-page_test.js
index acb1a8afe1b8..68fc3570a588 100644
--- a/client-src/elements/chromedash-enterprise-release-notes-page_test.js
+++ b/client-src/elements/chromedash-enterprise-release-notes-page_test.js
@@ -6,7 +6,7 @@ import './chromedash-toast';
import '../js-src/cs-client';
import sinon from 'sinon';
-describe('chromedash-feature-page', () => {
+describe('chromedash-enterprise-release-notes-page', () => {
const featuresPromise = Promise.resolve(
{
features: [
@@ -226,14 +226,14 @@ describe('chromedash-feature-page', () => {
beforeEach(async () => {
await fixture(html``);
window.csClient = new ChromeStatusClient('fake_token', 1);
- sinon.stub(window.csClient, 'searchFeatures');
+ sinon.stub(window.csClient, 'getFeaturesForEnterpriseReleaseNotes');
sinon.stub(window.csClient, 'getChannels');
- window.csClient.searchFeatures.returns(featuresPromise);
+ window.csClient.getFeaturesForEnterpriseReleaseNotes.returns(featuresPromise);
window.csClient.getChannels.returns(channelsPromise);
});
afterEach(() => {
- window.csClient.searchFeatures.restore();
+ window.csClient.getFeaturesForEnterpriseReleaseNotes.restore();
window.csClient.getChannels.restore();
clearURLParams('milestone');
});
@@ -258,7 +258,7 @@ describe('chromedash-feature-page', () => {
it('renders with no data', async () => {
const invalidFeaturePromise = Promise.reject(new Error('Got error response from server'));
- window.csClient.searchFeatures.returns(invalidFeaturePromise);
+ window.csClient.getFeaturesForEnterpriseReleaseNotes.returns(invalidFeaturePromise);
const component = await fixture(html`
`);
@@ -277,7 +277,6 @@ describe('chromedash-feature-page', () => {
`);
assert.exists(component);
assert.instanceOf(component, ChromedashEnterpriseReleaseNotesPage);
- debugger;
const releaseNotesSummary = component.shadowRoot.querySelector('#release-notes-summary');
const children = [...releaseNotesSummary.querySelectorAll('tr > *')];
@@ -473,6 +472,7 @@ describe('chromedash-feature-page', () => {
// Select a future milestone
component.selectedMilestone = 2000;
+ component.updateSelectedMilestone();
await nextFrame();
// Tests summary
diff --git a/client-src/elements/chromedash-feature-detail.js b/client-src/elements/chromedash-feature-detail.js
index 150113496be2..63a3d0d526ce 100644
--- a/client-src/elements/chromedash-feature-detail.js
+++ b/client-src/elements/chromedash-feature-detail.js
@@ -592,7 +592,7 @@ class ChromedashFeatureDetail extends LitElement {
// Show any buttons that should be displayed at the top of the detail card.
let addExtensionButton = nothing;
let editButton = nothing;
- let visitTrialButton = nothing;
+ const trialButton = this.renderOriginTrialButton(feStage);
if (this.canEdit && STAGE_TYPES_ORIGIN_TRIAL.has(feStage.stage_type)) {
// Button text changes based on whether or not an extension stage already exists.
const extensionAlreadyExists = (feStage.extensions && feStage.extensions.length > 0);
@@ -609,23 +609,9 @@ class ChromedashFeatureDetail extends LitElement {
href="/guide/stage/${this.feature.id}/${processStage.outgoing_stage}/${feStage.id}"
>Edit fields`;
}
- // If we have an origin trial ID associated with the stage, add a link to the trial.
- if (feStage.origin_trial_id) {
- let originTrialsURL = `https://origintrials-staging.corp.google.com/origintrials/#/view_trials/${feStage.origin_trial_id}`;
- // If this is the production host, link to the production OT site.
- if (this.appTitle === 'Chrome Platform Status') {
- originTrialsURL = `https://developer.chrome.com/origintrials/#/view_trials/${feStage.origin_trial_id}`;
- }
- visitTrialButton = html`
- View Origin Trial`;
- }
const content = html`
- ${visitTrialButton}
+ ${trialButton}
${editButton}
${addExtensionButton}
${processStage.description}
@@ -639,6 +625,53 @@ class ChromedashFeatureDetail extends LitElement {
return this.renderSection(name, content, isActive, defaultOpen);
}
+ renderOriginTrialButton(feStage) {
+ // Don't render an origin trial button if this is not an OT stage.
+ if (!STAGE_TYPES_ORIGIN_TRIAL.has(feStage.stage_type)) {
+ return nothing;
+ }
+
+ // If we have an origin trial ID associated with the stage, add a link to the trial.
+ if (feStage.origin_trial_id) {
+ let originTrialsURL = `https://origintrials-staging.corp.google.com/origintrials/#/view_trial/${feStage.origin_trial_id}`;
+ // If this is the production host, link to the production OT site.
+ if (this.appTitle === 'Chrome Platform Status') {
+ originTrialsURL = `https://developer.chrome.com/origintrials/#/view_trial/${feStage.origin_trial_id}`;
+ }
+ return html`
+ View Origin Trial`;
+ }
+ // TODO(DanielRyanSmith): uncomment this section to make the trial creation
+ // request form available for users.
+ /*
+ if (this.canEdit && feStage.ot_action_requested) {
+ // Display the button as disabled with tooltip text if a request
+ // has already been submitted.
+ return html`
+
+ Request Trial Creation
+ `;
+ // Display the creation request button if user has edit access.
+ } else if (this.canEdit) {
+ return html`
+ Request Trial Creation`;
+ }
+ */
+ return nothing;
+ }
+
renderAddStageButton() {
if (!this.canEdit) {
return nothing;
diff --git a/client-src/elements/chromedash-feature-page.js b/client-src/elements/chromedash-feature-page.js
index 7f7200582fb0..ff7e3694201e 100644
--- a/client-src/elements/chromedash-feature-page.js
+++ b/client-src/elements/chromedash-feature-page.js
@@ -141,7 +141,7 @@ export class ChromedashFeaturePage extends LitElement {
Promise.all([
window.csClient.getFeature(this.featureId),
window.csClient.getGates(this.featureId),
- window.csClient.getComments(this.featureId, null, false),
+ window.csClient.getComments(this.featureId, null),
window.csClient.getFeatureProcess(this.featureId),
window.csClient.getDismissedCues(),
window.csClient.getStars(),
@@ -194,7 +194,7 @@ export class ChromedashFeaturePage extends LitElement {
Promise.all([
window.csClient.getFeature(this.featureId),
window.csClient.getGates(this.featureId),
- window.csClient.getComments(this.featureId, null, false),
+ window.csClient.getComments(this.featureId, null),
]).then(([feature, gatesRes, commentRes]) => {
this.feature = feature;
this.gates = gatesRes.gates;
diff --git a/client-src/elements/chromedash-gate-chip.js b/client-src/elements/chromedash-gate-chip.js
index 1e0b77223d27..8d345cf60da5 100644
--- a/client-src/elements/chromedash-gate-chip.js
+++ b/client-src/elements/chromedash-gate-chip.js
@@ -170,13 +170,12 @@ class ChromedashGateChip extends LitElement {
return nothing;
}
const teamName = this.gate.team_name;
- const gateName = this.gate.name;
const stateName = GATE_STATE_TO_NAME[this.gate.state];
const className = stateName.toLowerCase().replaceAll(' ', '_');
const selected = (this.gate.id == this.selectedGateId) ? 'selected' : '';
const statusIconName = GATE_STATE_TO_ICON[this.gate.state];
- const abbrev = GATE_STATE_TO_ABBREV[this.gate.state] || gateName;
+ const abbrev = GATE_STATE_TO_ABBREV[this.gate.state] || stateName;
let statusIcon = html`${abbrev}`;
if (statusIconName) {
statusIcon = html`
@@ -185,14 +184,16 @@ class ChromedashGateChip extends LitElement {
`;
}
- const overdueIcon = (this.gate.slo_initial_response_remaining < 0) ?
+ const overdue = this.gate.slo_initial_response_remaining < 0;
+ const overdueIcon = overdue ?
html`` :
nothing;
+ const overdueTitle = overdue ? '. Overdue.' : '';
return html`
${statusIcon}
diff --git a/client-src/elements/chromedash-gate-column.js b/client-src/elements/chromedash-gate-column.js
index 1a795a6b9b20..c5339d115f36 100644
--- a/client-src/elements/chromedash-gate-column.js
+++ b/client-src/elements/chromedash-gate-column.js
@@ -170,7 +170,6 @@ export class ChromedashGateColumn extends LitElement {
window.csClient.getFeatureProcess(featureId),
window.csClient.getStage(featureId, stageId),
window.csClient.getVotes(featureId, null),
- // TODO(jrobbins): Include activities for this gate
window.csClient.getComments(featureId, gate.id),
]).then(([progress, process, stage, votesRes, commentRes]) => {
this.progress = progress;
diff --git a/client-src/elements/chromedash-header.js b/client-src/elements/chromedash-header.js
index 843442293da4..7dd36600a731 100644
--- a/client-src/elements/chromedash-header.js
+++ b/client-src/elements/chromedash-header.js
@@ -158,8 +158,8 @@ export class ChromedashHeader extends LitElement {
connectedCallback() {
super.connectedCallback();
- // The user sign-in is desktop only.
if (IS_MOBILE) {
+ // Login UI will be handled by chromedash-drawer instead.
return;
}
@@ -222,18 +222,13 @@ export class ChromedashHeader extends LitElement {
}
initializeTestingSignIn() {
- if (this.devMode != 'True') {
- return;
- }
-
// Create DEV_MODE login button for testing
const signInTestingButton = document.createElement('button');
signInTestingButton.innerText = 'Sign in as example@chromium.org';
signInTestingButton.setAttribute('type', 'button');
signInTestingButton.setAttribute('data-testid', 'dev-mode-sign-in-button');
- signInTestingButton.setAttribute('style', 'margin-right: 300px');
-
- const appComponent = document.querySelector('chromedash-app');
+ signInTestingButton.setAttribute('style',
+ 'margin-right: 300px; z-index:1000; background: lightblue; border: 1px solid blue;');
signInTestingButton.addEventListener('click', () => {
// POST to '/dev/mock_login' to login as example@chromium.
@@ -254,8 +249,9 @@ export class ChromedashHeader extends LitElement {
});
});
- if (appComponent) {
- appComponent.insertAdjacentElement('afterbegin', signInTestingButton); // for SPA
+ const signInButtonContainer = document.querySelector('chromedash-app');
+ if (signInButtonContainer) {
+ signInButtonContainer.insertAdjacentElement('afterbegin', signInTestingButton); // for SPA
} else {
this.insertAdjacentElement('afterbegin', signInTestingButton); // for MPA
}
@@ -339,7 +335,7 @@ export class ChromedashHeader extends LitElement {
return html`
-