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`
${this.user.email}
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`

-