Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 5c96901

Browse files
authored
Merge branch 'develop' into overlay-pagination
2 parents 823a05e + b8482b6 commit 5c96901

File tree

3 files changed

+122
-12
lines changed

3 files changed

+122
-12
lines changed

src/components/structures/InteractiveAuth.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,19 +209,24 @@ export default class InteractiveAuthComponent<T> extends React.Component<Interac
209209

210210
private onBusyChanged = (busy: boolean): void => {
211211
// if we've started doing stuff, reset the error messages
212+
// The JS SDK eagerly reports itself as "not busy" right after any
213+
// immediate work has completed, but that's not really what we want at
214+
// the UI layer, so we ignore this signal and show a spinner until
215+
// there's a new screen to show the user. This is implemented by setting
216+
// `busy: false` in `authStateUpdated`.
217+
// See also https://github.com/vector-im/element-web/issues/12546
212218
if (busy) {
213219
this.setState({
214220
busy: true,
215221
errorText: undefined,
216222
errorCode: undefined,
217223
});
218224
}
219-
// The JS SDK eagerly reports itself as "not busy" right after any
220-
// immediate work has completed, but that's not really what we want at
221-
// the UI layer, so we ignore this signal and show a spinner until
222-
// there's a new screen to show the user. This is implemented by setting
223-
// `busy: false` in `authStateUpdated`.
224-
// See also https://github.com/vector-im/element-web/issues/12546
225+
226+
// authStateUpdated is not called during sso flows
227+
if (!busy && (this.state.authStage === AuthType.Sso || this.state.authStage === AuthType.SsoUnstable)) {
228+
this.setState({ busy });
229+
}
225230
};
226231

227232
private setFocus(): void {

src/components/views/auth/InteractiveAuthEntryComponents.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,7 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
861861

862862
public render(): React.ReactNode {
863863
let continueButton: JSX.Element;
864+
864865
const cancelButton = (
865866
<AccessibleButton
866867
onClick={this.props.onCancel ?? null}
@@ -902,8 +903,14 @@ export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEn
902903
<Fragment>
903904
{errorSection}
904905
<div className="mx_InteractiveAuthEntryComponents_sso_buttons">
905-
{cancelButton}
906-
{continueButton}
906+
{this.props.busy ? (
907+
<Spinner w={24} h={24} />
908+
) : (
909+
<>
910+
{cancelButton}
911+
{continueButton}
912+
</>
913+
)}
907914
</div>
908915
</Fragment>
909916
);

test/components/views/dialogs/InteractiveAuthDialog-test.tsx

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@ limitations under the License.
1616
*/
1717

1818
import React from "react";
19-
import { render, screen } from "@testing-library/react";
19+
import { fireEvent, render, screen, act } from "@testing-library/react";
2020
import userEvent from "@testing-library/user-event";
21+
import { mocked } from "jest-mock";
2122

2223
import InteractiveAuthDialog from "../../../../src/components/views/dialogs/InteractiveAuthDialog";
23-
import { flushPromises, getMockClientWithEventEmitter, unmockClientPeg } from "../../../test-utils";
24+
import { clearAllModals, flushPromises, getMockClientWithEventEmitter, unmockClientPeg } from "../../../test-utils";
2425

2526
describe("InteractiveAuthDialog", function () {
27+
const homeserverUrl = "https://matrix.org";
28+
const authUrl = "https://auth.com";
2629
const mockClient = getMockClientWithEventEmitter({
2730
generateClientSecret: jest.fn().mockReturnValue("t35tcl1Ent5ECr3T"),
31+
getFallbackAuthUrl: jest.fn().mockReturnValue(authUrl),
32+
getHomeserverUrl: jest.fn().mockReturnValue(homeserverUrl),
2833
});
2934

3035
const defaultProps = {
@@ -37,13 +42,15 @@ describe("InteractiveAuthDialog", function () {
3742
const getPasswordField = () => screen.getByLabelText("Password");
3843
const getSubmitButton = () => screen.getByRole("button", { name: "Continue" });
3944

40-
beforeEach(function () {
45+
beforeEach(async function () {
4146
jest.clearAllMocks();
4247
mockClient.credentials = { userId: null };
48+
await clearAllModals();
4349
});
4450

45-
afterAll(() => {
51+
afterAll(async () => {
4652
unmockClientPeg();
53+
await clearAllModals();
4754
});
4855

4956
it("Should successfully complete a password flow", async () => {
@@ -94,4 +101,95 @@ describe("InteractiveAuthDialog", function () {
94101
expect(onFinished).toHaveBeenCalledTimes(1);
95102
expect(onFinished).toHaveBeenCalledWith(true, { a: 1 });
96103
});
104+
105+
describe("SSO flow", () => {
106+
it("should close on cancel", () => {
107+
const onFinished = jest.fn();
108+
const makeRequest = jest.fn().mockResolvedValue({ a: 1 });
109+
110+
mockClient.credentials = { userId: "@user:id" };
111+
const authData = {
112+
session: "sess",
113+
flows: [{ stages: ["m.login.sso"] }],
114+
};
115+
116+
renderComponent({ makeRequest, onFinished, authData });
117+
118+
expect(screen.getByText("To continue, use Single Sign On to prove your identity.")).toBeInTheDocument();
119+
120+
fireEvent.click(screen.getByText("Cancel"));
121+
122+
expect(onFinished).toHaveBeenCalledWith(false, null);
123+
});
124+
125+
it("should complete an sso flow", async () => {
126+
jest.spyOn(global.window, "addEventListener");
127+
// @ts-ignore
128+
jest.spyOn(global.window, "open").mockImplementation(() => {});
129+
const onFinished = jest.fn();
130+
const successfulResult = { test: 1 };
131+
const makeRequest = jest
132+
.fn()
133+
.mockRejectedValueOnce({ httpStatus: 401, data: { flows: [{ stages: ["m.login.sso"] }] } })
134+
.mockResolvedValue(successfulResult);
135+
136+
mockClient.credentials = { userId: "@user:id" };
137+
const authData = {
138+
session: "sess",
139+
flows: [{ stages: ["m.login.sso"] }],
140+
};
141+
142+
renderComponent({ makeRequest, onFinished, authData });
143+
144+
await flushPromises();
145+
146+
expect(screen.getByText("To continue, use Single Sign On to prove your identity.")).toBeInTheDocument();
147+
fireEvent.click(screen.getByText("Single Sign On"));
148+
149+
// no we're on the sso auth screen
150+
expect(screen.getByText("Click the button below to confirm your identity.")).toBeInTheDocument();
151+
152+
// launch sso
153+
fireEvent.click(screen.getByText("Confirm"));
154+
expect(global.window.open).toHaveBeenCalledWith(authUrl, "_blank");
155+
156+
const onWindowReceiveMessageCall = mocked(window.addEventListener).mock.calls.find(
157+
(args) => args[0] === "message",
158+
);
159+
expect(onWindowReceiveMessageCall).toBeTruthy();
160+
// get the handle from SSO auth component
161+
// so we can pretend sso auth was completed
162+
const onWindowReceiveMessage = onWindowReceiveMessageCall![1];
163+
164+
// complete sso successfully
165+
act(() => {
166+
// @ts-ignore
167+
onWindowReceiveMessage({ data: "authDone", origin: homeserverUrl });
168+
});
169+
170+
// expect(makeRequest).toHaveBeenCalledWith({ session: authData.session })
171+
172+
// spinner displayed
173+
expect(screen.getByRole("progressbar")).toBeInTheDocument();
174+
// cancel/confirm buttons hidden while request in progress
175+
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
176+
177+
await flushPromises();
178+
await flushPromises();
179+
180+
// nothing in progress
181+
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
182+
183+
// auth completed, now make the request again with auth
184+
fireEvent.click(screen.getByText("Confirm"));
185+
// loading while making request
186+
expect(screen.getByRole("progressbar")).toBeInTheDocument();
187+
188+
expect(makeRequest).toHaveBeenCalledTimes(2);
189+
190+
await flushPromises();
191+
192+
expect(onFinished).toHaveBeenCalledWith(true, successfulResult);
193+
});
194+
});
97195
});

0 commit comments

Comments
 (0)