diff --git a/apps/src/signIn/SectionCodeEntry.tsx b/apps/src/signIn/SectionCodeEntry.tsx new file mode 100644 index 0000000000000..dbb66eb394b66 --- /dev/null +++ b/apps/src/signIn/SectionCodeEntry.tsx @@ -0,0 +1,53 @@ +import TextField from '@code-dot-org/component-library/textField'; +import {Button as MuiButton} from '@mui/material'; +import React, {useState} from 'react'; + +import style from './signInStyles.module.scss'; + +export interface SectionCodeEntryProps { + // Field label, e.g. "Enter your 6 letter section code". Rendered as the + // TextField's own label so it matches the sign-in fields' labels exactly + // (same size, weight, padding, and top alignment). + sectionCodeLabel: string; + sectionCodePlaceholder: string; + defaultSectionCode: string; + goLabel: string; + // Where the GET form submits (student_user_new_path -> /users/new). + formAction: string; +} + +const SectionCodeEntry: React.FunctionComponent = ({ + sectionCodeLabel, + sectionCodePlaceholder, + defaultSectionCode, + goLabel, + formAction, +}) => { + const [sectionCode, setSectionCode] = useState(defaultSectionCode || ''); + + return ( +
+
+ setSectionCode(e.target.value)} + /> + + {goLabel} + + +
+ ); +}; + +export default SectionCodeEntry; diff --git a/apps/src/signIn/SignInForm.tsx b/apps/src/signIn/SignInForm.tsx new file mode 100644 index 0000000000000..431712a87ad31 --- /dev/null +++ b/apps/src/signIn/SignInForm.tsx @@ -0,0 +1,152 @@ +import Link from '@code-dot-org/component-library/link'; +import TextField from '@code-dot-org/component-library/textField'; +import {Button as MuiButton} from '@mui/material'; +import React, {useEffect, useState} from 'react'; + +import RailsAuthenticityToken from '@cdo/apps/lib/util/RailsAuthenticityToken'; +import {EVENTS} from '@cdo/apps/metrics/AnalyticsConstants'; +import analyticsReporter from '@cdo/apps/metrics/AnalyticsReporter'; +import {USER_RETURN_TO_SESSION_KEY} from '@cdo/apps/signUpFlow/signUpFlowConstants'; + +import style from './signInStyles.module.scss'; + +// The field names below (user[login], user[password], user[hashed_email]) +// match what the Rails controller expects. + +// These ids double as the DOM hooks the UI (Cucumber) tests drive: +// #signin (wrapper), #user_login, #user_password, #signin-button. +const LOGIN_FIELD_ID = 'user_login'; +const PASSWORD_FIELD_ID = 'user_password'; + +export interface SignInFormProps { + // hashed_email is populated server-side (on failed-login re-render) and + // threaded through untouched -- sign-in never hashes the email client-side. + hashedEmail: string; + // Pre-populated login value (from @email), if any. + loginValue: string; + // Form action from the server (session_path) so regional/localized sign-in + // routes (e.g. /fa/users/sign_in) are preserved. + signInPath: string; + loginLabel: string; + passwordLabel: string; + signInLabel: string; + signUpLabel: string; + signUpPath: string; + // Whether the "Sign up" button should render (devise_mapping.registerable?). + showSignUp: boolean; + // Devise password-reset path; absent when the mapping is not recoverable. + forgotPasswordPath?: string; + forgotPasswordLabel?: string; + // Written to sessionStorage on mount when present, before any redirect. + userReturnTo?: string | null; +} + +const SignInForm: React.FunctionComponent = ({ + hashedEmail, + loginValue, + signInPath, + loginLabel, + passwordLabel, + signInLabel, + signUpLabel, + signUpPath, + showSignUp, + forgotPasswordPath, + forgotPasswordLabel, + userReturnTo, +}) => { + const [login, setLogin] = useState(loginValue || ''); + const [password, setPassword] = useState(''); + + // Match the legacy autofocus behavior: focus the login field when there is + // no pre-filled email, otherwise focus the password field. Done via an + // effect rather than the autoFocus prop (which jsx-a11y disallows). + const emailPrefilled = (loginValue || '') !== ''; + + useEffect(() => { + if (userReturnTo) { + sessionStorage.setItem(USER_RETURN_TO_SESSION_KEY, userReturnTo); + } + }, [userReturnTo]); + + useEffect(() => { + const id = emailPrefilled ? PASSWORD_FIELD_ID : LOGIN_FIELD_ID; + document.getElementById(id)?.focus(); + }, [emailPrefilled]); + + return ( +
+
+ {/* + Do NOT set id="new_user" on this form. devise/sessions/new.js hooks + form#new_user's submit to run window.dashboard.hashEmail against + #user_hashed_email (which this React form doesn't have), which breaks + the native POST and login never completes. The region-versioned action + is verified server-side via the mount's data-sign-in-path instead. + */} +
+ + + + setLogin(e.target.value)} + autoComplete="username" + /> + setPassword(e.target.value)} + autoComplete="current-password" + /> + + {forgotPasswordPath && forgotPasswordLabel && ( + + {forgotPasswordLabel} + + )} + + + {signInLabel} + + + + {showSignUp && ( + + analyticsReporter.sendEvent( + EVENTS.LOGIN_PAGE_CREATE_ACCOUNT_CLICKED, + {} + ) + } + > + {signUpLabel} + + )} +
+
+ ); +}; + +export default SignInForm; diff --git a/apps/src/signIn/signInStyles.module.scss b/apps/src/signIn/signInStyles.module.scss new file mode 100644 index 0000000000000..3c6c462769830 --- /dev/null +++ b/apps/src/signIn/signInStyles.module.scss @@ -0,0 +1,59 @@ +// Layout for the sign-in page. The DSCO/MUI components own their own colors +// through semantic tokens, so this module owns layout only -- there are no raw +// hex values or legacy SCSS color variables here. + +.signInColumn { + display: flex; + // Match the legacy #signin min-width so the sign-in and OAuth columns split + // the row evenly. Without it the OAuth column (flex-grow: 2, uncapped) grows + // to fill the extra space and dwarfs the sign-in form. + flex-grow: 2; + min-width: 450px; +} + +.formArea { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; +} + +.form { + display: flex; + flex-direction: column; + gap: 1rem; + margin: 0; +} + +.field { + width: 100%; + + input { + width: 100%; + } +} + +.forgotPassword { + align-self: flex-start; +} + +.sectionCode { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.sectionCodeForm { + display: flex; + align-items: flex-end; + gap: 0.5rem; + margin: 0; +} + +.sectionCodeField { + flex-grow: 1; + + input { + width: 100%; + } +} diff --git a/apps/src/sites/studio/pages/devise/sessions/_login.js b/apps/src/sites/studio/pages/devise/sessions/_login.js index 9416bab00ed54..0dd8cf0e7ef62 100644 --- a/apps/src/sites/studio/pages/devise/sessions/_login.js +++ b/apps/src/sites/studio/pages/devise/sessions/_login.js @@ -1,21 +1,69 @@ +import {Typography} from '@mui/material'; import $ from 'jquery'; +import React from 'react'; import {EVENTS} from '@cdo/apps/metrics/AnalyticsConstants'; import analyticsReporter from '@cdo/apps/metrics/AnalyticsReporter'; -import {USER_RETURN_TO_SESSION_KEY} from '@cdo/apps/signUpFlow/signUpFlowConstants'; -import getScriptData from '@cdo/apps/util/getScriptData'; +import SectionCodeEntry from '@cdo/apps/signIn/SectionCodeEntry'; +import SignInForm from '@cdo/apps/signIn/SignInForm'; +import {createReactRoot} from '@cdo/apps/util/createReactRoot'; $(document).ready(() => { - const userReturnTo = getScriptData('userReturnTo'); + // Page title (full-width, above both columns). Rendered as MUI Typography so + // it matches the design system rather than the legacy global

styles. + const titleMount = document.getElementById('sign-in-title'); + if (titleMount) { + createReactRoot( + + {titleMount.dataset.title} + , + titleMount, + {legacyReactDomRender: true} + ); + } - if (userReturnTo) { - sessionStorage.setItem(USER_RETURN_TO_SESSION_KEY, userReturnTo); + const signInMount = document.getElementById('sign-in-page-layout'); + if (signInMount) { + const data = signInMount.dataset; + createReactRoot( + , + signInMount, + {legacyReactDomRender: true} + ); } - document.getElementById('user_signup').addEventListener('click', () => { - analyticsReporter.sendEvent(EVENTS.LOGIN_PAGE_CREATE_ACCOUNT_CLICKED, {}); - }); + // Only present on the sessions (sign-in) page. + const sectionCodeMount = document.getElementById('section-code-entry-mount'); + if (sectionCodeMount) { + const data = sectionCodeMount.dataset; + createReactRoot( + , + sectionCodeMount, + {legacyReactDomRender: true} + ); + } + // Course blocks remain server-rendered HAML for now, so keep their + // click analytics wiring here until they are migrated separately. const courseBlocks = document.querySelectorAll('.courseblock-tall'); courseBlocks.forEach(courseBlock => { const courseTitle = courseBlock.querySelector('h3').textContent; diff --git a/apps/test/unit/signIn/SectionCodeEntryTest.tsx b/apps/test/unit/signIn/SectionCodeEntryTest.tsx new file mode 100644 index 0000000000000..c9e9cfaab252b --- /dev/null +++ b/apps/test/unit/signIn/SectionCodeEntryTest.tsx @@ -0,0 +1,46 @@ +import {render, screen} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import React from 'react'; + +import SectionCodeEntry, { + SectionCodeEntryProps, +} from '@cdo/apps/signIn/SectionCodeEntry'; + +const DEFAULT_PROPS: SectionCodeEntryProps = { + sectionCodeLabel: 'Enter your 6 letter section code', + sectionCodePlaceholder: 'Section Code (ABCDEF)', + defaultSectionCode: '', + goLabel: 'Go', + formAction: '/users/new', +}; + +describe('SectionCodeEntry', () => { + function renderEntry(overrides: Partial = {}) { + return render(); + } + + it('renders the labeled section-code input and Go button', () => { + renderEntry(); + + expect( + screen.getByRole('textbox', {name: DEFAULT_PROPS.sectionCodeLabel}) + ).toHaveAttribute('name', 'section_code'); + screen.getByRole('button', {name: DEFAULT_PROPS.goLabel}); + }); + + it('pre-populates the input with defaultSectionCode when provided', () => { + renderEntry({defaultSectionCode: 'ABCDEF'}); + expect( + screen.getByRole('textbox', {name: DEFAULT_PROPS.sectionCodeLabel}) + ).toHaveValue('ABCDEF'); + }); + + it('submits via GET to the provided form action', () => { + renderEntry({formAction: '/users/new'}); + const form = screen + .getByRole('button', {name: DEFAULT_PROPS.goLabel}) + .closest('form'); + expect(form).toHaveAttribute('action', '/users/new'); + expect(form).toHaveAttribute('method', 'get'); + }); +}); diff --git a/apps/test/unit/signIn/SignInFormTest.tsx b/apps/test/unit/signIn/SignInFormTest.tsx new file mode 100644 index 0000000000000..ad773c7f1e0a9 --- /dev/null +++ b/apps/test/unit/signIn/SignInFormTest.tsx @@ -0,0 +1,126 @@ +import {render, screen, fireEvent} from '@testing-library/react'; +import '@testing-library/jest-dom'; +import React from 'react'; + +import {EVENTS} from '@cdo/apps/metrics/AnalyticsConstants'; +import analyticsReporter from '@cdo/apps/metrics/AnalyticsReporter'; +import SignInForm, {SignInFormProps} from '@cdo/apps/signIn/SignInForm'; +import {USER_RETURN_TO_SESSION_KEY} from '@cdo/apps/signUpFlow/signUpFlowConstants'; + +const DEFAULT_PROPS: SignInFormProps = { + hashedEmail: 'hashed-abc', + loginValue: '', + signInPath: '/users/sign_in', + loginLabel: 'Email address or username', + passwordLabel: 'Password', + signInLabel: 'Sign in', + signUpLabel: 'Sign up', + signUpPath: '/users/sign_up/account_type', + showSignUp: true, + forgotPasswordPath: '/users/password/new', + forgotPasswordLabel: 'Forgot password?', + userReturnTo: null, +}; + +describe('SignInForm', () => { + beforeAll(() => { + // RailsAuthenticityToken reads these meta tags; provide them so it renders + // the token input instead of logging a missing-tags error. + document.head.innerHTML = + '' + + ''; + }); + + beforeEach(() => { + sessionStorage.clear(); + }); + + function renderForm(overrides: Partial = {}) { + return render(); + } + + it('renders login and password fields and the sign-in button', () => { + renderForm(); + + expect(screen.getByLabelText(DEFAULT_PROPS.loginLabel)).toHaveAttribute( + 'name', + 'user[login]' + ); + expect(screen.getByLabelText(DEFAULT_PROPS.passwordLabel)).toHaveAttribute( + 'name', + 'user[password]' + ); + screen.getByRole('button', {name: DEFAULT_PROPS.signInLabel}); + }); + + it('posts to signInPath (preserves the regional/localized action)', () => { + renderForm({signInPath: '/fa/users/sign_in'}); + const form = screen + .getByRole('button', {name: DEFAULT_PROPS.signInLabel}) + .closest('form'); + expect(form).toHaveAttribute('action', '/fa/users/sign_in'); + expect(form).toHaveAttribute('method', 'post'); + // Must NOT be id="new_user": devise/sessions/new.js hooks that form's + // submit to hash the email, which breaks this React-native POST. + expect(form).not.toHaveAttribute('id', 'new_user'); + }); + + it('threads the hashed_email through as a hidden field, unchanged', () => { + renderForm({hashedEmail: 'server-set-hash'}); + const hidden = screen.getByDisplayValue('server-set-hash'); + expect(hidden).toHaveAttribute('name', 'user[hashed_email]'); + expect(hidden).toHaveAttribute('type', 'hidden'); + }); + + it('renders the sign-up button and fires the analytics event on click', () => { + const sendEventSpy = jest.spyOn(analyticsReporter, 'sendEvent'); + renderForm(); + + fireEvent.click(screen.getByText(DEFAULT_PROPS.signUpLabel)); + + expect(sendEventSpy).toHaveBeenCalledWith( + EVENTS.LOGIN_PAGE_CREATE_ACCOUNT_CLICKED, + {} + ); + sendEventSpy.mockRestore(); + }); + + it('does not render the sign-up button when showSignUp is false', () => { + renderForm({showSignUp: false}); + expect(screen.queryByText(DEFAULT_PROPS.signUpLabel)).toBeNull(); + }); + + it('points the forgot-password link at forgotPasswordPath', () => { + renderForm({ + forgotPasswordPath: '/users/password/new', + forgotPasswordLabel: 'Forgot password?', + }); + expect( + screen.getByRole('link', {name: 'Forgot password?'}) + ).toHaveAttribute('href', '/users/password/new'); + }); + + it('omits the forgot-password link when no path is provided', () => { + renderForm({forgotPasswordPath: undefined}); + expect( + screen.queryByText(DEFAULT_PROPS.forgotPasswordLabel as string) + ).toBeNull(); + }); + + it('writes userReturnTo to sessionStorage on mount when set', () => { + renderForm({userReturnTo: '/home'}); + expect(sessionStorage.getItem(USER_RETURN_TO_SESSION_KEY)).toBe('/home'); + }); + + it('does not write userReturnTo to sessionStorage when absent', () => { + renderForm({userReturnTo: null}); + expect(sessionStorage.getItem(USER_RETURN_TO_SESSION_KEY)).toBeNull(); + }); + + it('pre-populates the login field with loginValue', () => { + renderForm({loginValue: 'teacher@example.com'}); + expect(screen.getByLabelText(DEFAULT_PROPS.loginLabel)).toHaveValue( + 'teacher@example.com' + ); + }); +}); diff --git a/dashboard/app/assets/stylesheets/application.scss b/dashboard/app/assets/stylesheets/application.scss index a1d32ffcaaf47..98035d88c8ec4 100644 --- a/dashboard/app/assets/stylesheets/application.scss +++ b/dashboard/app/assets/stylesheets/application.scss @@ -1878,7 +1878,13 @@ a.download-video { max-width: 600px; } -#signin, +// The React sign-in page (apps/src/signIn/) styles itself, so #signin no longer +// carries these rules. They remain for #change-password (password reset) and +// for the legacy server-rendered sign-in/sign-up pages still using this markup +// (existing_account, PD/Foorm logged-out pages), which opt in via .signin-legacy. +// Don't re-add #signin here: the React page reuses that id as a test hook and +// must not inherit the legacy form/button styling. +.signin-legacy, #change-password { $field-width: 310px; $button-width: 324px; @@ -2405,48 +2411,8 @@ a.download-video { margin: 0; } - .section-sign-in { - display: flex; - align-items: center; - - input { - background-color: $neutral_light; - } - - input[type=text] { - flex-grow: 2; - height: 40px; - padding-top: 0; - padding-bottom: 0; - margin: 0; - } - - button { - @include main-font-semi-bold; - padding: 10px 20px; - margin-right: 0; - background-color: $brand_secondary_default; - border: 2px solid $brand_secondary_default; - color: $neutral_white; - - &:hover { - background-color: $brand_secondary_dark; - border-color: $brand_secondary_dark; - box-shadow: none; - } - &:focus { - border-color: $brand_primary_default; - } - - &:disabled { - color: $neutral_white; - border-color: $neutral_dark20; - background-color: $neutral_dark20; - @include box-shadow(none); - cursor: not-allowed; - } - } - } + // .section-sign-in retired: section-code entry now lives in React + // (apps/src/signIn/SectionCodeEntry.tsx). .oauth-sign-in { @include main-font-semi-bold; diff --git a/dashboard/app/views/devise/registrations/existing_account.html.haml b/dashboard/app/views/devise/registrations/existing_account.html.haml index 0e16a84c687b0..050587431a475 100644 --- a/dashboard/app/views/devise/registrations/existing_account.html.haml +++ b/dashboard/app/views/devise/registrations/existing_account.html.haml @@ -1,7 +1,7 @@ %h1 = I18n.t('auth.existing_account.account_exists', email: params[:email]) -#signin +#signin.signin-legacy %p = I18n.t('auth.existing_account.sign_in', provider: I18n.t(params[:provider], scope: "auth")) diff --git a/dashboard/app/views/devise/sessions/_login.html.haml b/dashboard/app/views/devise/sessions/_login.html.haml index 2f68b64f4fd2a..e392cbbd79325 100644 --- a/dashboard/app/views/devise/sessions/_login.html.haml +++ b/dashboard/app/views/devise/sessions/_login.html.haml @@ -1,39 +1,33 @@ - content_for :head do - :ruby - script_data = {userReturnTo: @user_return_to.presence.to_json} - %script{src: webpack_asset_path('js/devise/sessions/_login.js'), data: script_data} + %script{src: webpack_asset_path('js/devise/sessions/_login.js')} -%h2= t('signin_form.title') +/ Page title, rendered as MUI Typography by devise/sessions/_login.js. +#sign-in-title{data: {title: t('signin_form.title')}} + +/ Login errors (e.g. "invalid password") are still delivered via the Rails +/ flash. show_flashes consumes the flash the first time it is called, so +/ keeping it here (the view renders before the layout) is what actually +/ surfaces the error on this page. Flash migration is a separate ticket. += show_flashes.html_safe .flex-container - #signin - = form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| - = show_flashes.html_safe - = f.hidden_field :hashed_email - - / Email - .field - = label_tag t('signin_form.login_username') - - email = @email || '' - = f.text_field :login, value: email, autofocus: email == '' - - / Password - .field#password_field - = f.label :password - = f.password_field :password, autofocus: email != '' - - / Forgot password? - - if devise_mapping.recoverable? - %div.field-aligned.password_help_link - = link_to t('auth.forgot_password'), new_password_path(resource_name), id: 'forgot-password' - - / Sign in button - %button#signin-button= t('signin_form.submit') - - / Not yet signed up? Sign up - - if devise_mapping.registerable? - %a#signup-link.field-aligned{href: users_sign_up_account_type_path} - %button.neutral-button{:id => 'user_signup'}= t('nav.user.signup') + / Left column: sign-in form + sign-up button (React). display:contents makes + / the mounted component's own root, not this div, the flex child. + #sign-in-page-layout{style: 'display: contents', + data: { + hashed_email: resource.hashed_email, + login_value: (@email || ''), + sign_in_path: session_path(resource_name), + login_label: t('signin_form.login_username'), + password_label: resource.class.human_attribute_name(:password), + sign_in_label: t('signin_form.submit'), + sign_up_label: t('nav.user.signup'), + sign_up_path: users_sign_up_account_type_path, + show_sign_up: devise_mapping.registerable?.to_s, + forgot_password_path: (new_password_path(resource_name) if devise_mapping.recoverable?), + forgot_password_label: t('auth.forgot_password'), + user_return_to: @user_return_to.presence, + }} %div.vertical-or %hr diff --git a/dashboard/app/views/devise/shared/_oauth_links.haml b/dashboard/app/views/devise/shared/_oauth_links.haml index 56ea2c0b24853..1aa3599ae6881 100644 --- a/dashboard/app/views/devise/shared/_oauth_links.haml +++ b/dashboard/app/views/devise/shared/_oauth_links.haml @@ -9,14 +9,21 @@ %br/ - if controller_name == 'sessions' - %h6= t('join_section.code.instructions_short') - = form_tag(student_user_new_path, method: :get, class: 'section-sign-in') do - = text_field_tag :section_code, params[:section_code], placeholder: t('join_section.code.placeholder') - %button= 'Go' + / Section-code entry (React). Mounted by devise/sessions/_login.js. + #section-code-entry-mount{ + data: { + section_code_label: t('join_section.code.instructions_short'), + section_code_placeholder: t('join_section.code.placeholder'), + default_section_code: (params[:section_code] || ''), + go_label: 'Go', + form_action: student_user_new_path, + } + } - if devise_mapping.omniauthable? - unless Rails.env.production? - %strong + / display:block is required so the vertical margin applies (strong is inline). + %strong{style: 'display: block; margin-block: 0.5rem'} Hi! If you are having trouble with OAuth features on = "#{Rails.env}," consult the "Developer OAuth Setup on Localhost" doc in the Engineering drive folder diff --git a/dashboard/app/views/foorm/simple_survey_forms/logged_out.haml b/dashboard/app/views/foorm/simple_survey_forms/logged_out.haml index 2211f0ecc942d..3081f1702b6c5 100644 --- a/dashboard/app/views/foorm/simple_survey_forms/logged_out.haml +++ b/dashboard/app/views/foorm/simple_survey_forms/logged_out.haml @@ -8,7 +8,7 @@ .paragraph Please sign in or create an account: - #signin + #signin.signin-legacy = link_to "/users/sign_in?user_return_to=#{request.fullpath}", class: "paragraph" do %button= I18n.t('nav.user.signin') diff --git a/dashboard/app/views/pd/application/teacher_application/logged_out.haml b/dashboard/app/views/pd/application/teacher_application/logged_out.haml index 572465fde664b..698f4a5b4b7cd 100644 --- a/dashboard/app/views/pd/application/teacher_application/logged_out.haml +++ b/dashboard/app/views/pd/application/teacher_application/logged_out.haml @@ -24,7 +24,7 @@ .paragraph Please sign in or create an account: - #signin + #signin.signin-legacy = link_to "/users/sign_in?user_return_to=#{request.fullpath}", class: "paragraph" do %button= I18n.t('nav.user.signin') diff --git a/dashboard/app/views/pd/misc_survey/logged_out.haml b/dashboard/app/views/pd/misc_survey/logged_out.haml index dbe442f0d1785..73448e950dccb 100644 --- a/dashboard/app/views/pd/misc_survey/logged_out.haml +++ b/dashboard/app/views/pd/misc_survey/logged_out.haml @@ -11,7 +11,7 @@ .paragraph Please sign in or create an account: - #signin + #signin.signin-legacy = link_to "/users/sign_in?user_return_to=#{request.fullpath}", class: "paragraph" do %button= I18n.t('nav.user.signin') diff --git a/dashboard/test/integration/global_edition_test.rb b/dashboard/test/integration/global_edition_test.rb index 59bb73620e37c..46438a150ff7d 100644 --- a/dashboard/test/integration/global_edition_test.rb +++ b/dashboard/test/integration/global_edition_test.rb @@ -141,8 +141,11 @@ class GlobalEditionTest < ActionDispatch::IntegrationTest it 'routing helpers generates region version of urls' do get_regional_page - new_user_button = must_select("form#new_user[method='post']").first - _(new_user_button['action']).must_equal regional_page_path + # The sign-in form is React-rendered now, so the region-versioned action + # (session_path) is handed to the mount point as a data attribute rather + # than a server-rendered form[action]. + sign_in_mount = must_select('#sign-in-page-layout').first + _(sign_in_mount['data-sign-in-path']).must_equal regional_page_path end context 'for signed-in user' do diff --git a/dashboard/test/ui/features/platform/signing_in.feature b/dashboard/test/ui/features/platform/signing_in.feature index 882562d18b69e..c24c305eeec24 100644 --- a/dashboard/test/ui/features/platform/signing_in.feature +++ b/dashboard/test/ui/features/platform/signing_in.feature @@ -44,5 +44,5 @@ Scenario: Signed-out joining non-picture non-word section from sign in page goes Given I sign out Given I am on "http://studio.code.org/users/sign_in/" And I type the section code into "#section_code" - And I click ".section-sign-in button" to load a new page + And I click "#section_code_submit" to load a new page And I wait until element "a:contains(Create an account)" is visible