diff --git a/src/course-home/data/__factories__/courseBlocks.factory.js b/src/course-home/data/__factories__/courseBlocks.factory.js
index 1fa6dd90..a31a4090 100644
--- a/src/course-home/data/__factories__/courseBlocks.factory.js
+++ b/src/course-home/data/__factories__/courseBlocks.factory.js
@@ -61,10 +61,16 @@ export default function buildSimpleCourseBlocks(courseId, title, options = {}) {
)];
const sectionBlock = options.sectionBlock || Factory.build(
'block',
- { type: 'chapter', children: sequenceBlock.map(block => block.id), resume_block: false },
+ {
+ type: 'chapter',
+ display_name: 'Title of Section',
+ complete: options.complete || false,
+ resume_block: options.resumeBlock || false,
+ children: sequenceBlock.map(block => block.id),
+ },
{ courseId },
);
- const courseBlock = options.courseBlocks || Factory.build(
+ const courseBlock = options.courseBlock || Factory.build(
'block',
{ type: 'course', display_name: title, children: [sectionBlock.id] },
{ courseId },
diff --git a/src/course-home/data/__factories__/outlineTabData.factory.js b/src/course-home/data/__factories__/outlineTabData.factory.js
index da622209..61476826 100644
--- a/src/course-home/data/__factories__/outlineTabData.factory.js
+++ b/src/course-home/data/__factories__/outlineTabData.factory.js
@@ -19,7 +19,7 @@ Factory.define('outlineTabData')
})
.attr('course_goals', [], () => ({
goal_options: [],
- selected_goal: {},
+ selected_goal: null,
}))
.attr('enroll_alert', {
can_enroll: true,
diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap
index b88bb17d..08c8efe5 100644
--- a/src/course-home/data/__snapshots__/redux.test.js.snap
+++ b/src/course-home/data/__snapshots__/redux.test.js.snap
@@ -358,7 +358,7 @@ Object {
"sequenceIds": Array [
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
],
- "title": "bcdabcdabcdabcdabcdabcdabcdabcd2",
+ "title": "Title of Section",
},
},
"sequences": Object {
@@ -377,7 +377,7 @@ Object {
"courseExpiredHtml": "
Course expired
",
"courseGoals": Object {
"goalOptions": Array [],
- "selectedGoal": Object {},
+ "selectedGoal": null,
},
"courseTools": Array [
Object {
diff --git a/src/course-home/dates-tab/DatesTab.test.jsx b/src/course-home/dates-tab/DatesTab.test.jsx
index 5d1fb0eb..03bf7f2d 100644
--- a/src/course-home/dates-tab/DatesTab.test.jsx
+++ b/src/course-home/dates-tab/DatesTab.test.jsx
@@ -19,31 +19,26 @@ import { UserMessagesProvider } from '../../generic/user-messages';
initializeMockApp();
describe('DatesTab', () => {
- let store;
- let component;
let axiosMock;
- let courseId;
+
+ const store = initializeStore();
+ const component = (
+
+
+
+
+
+
+
+
+
+ );
+ const courseMetadata = Factory.build('courseHomeMetadata');
+ const { courseId } = courseMetadata;
beforeEach(() => {
- store = initializeStore();
- component = (
-
-
-
-
-
-
-
-
-
- );
-
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
-
- const courseMetadata = Factory.build('courseHomeMetadata');
- courseId = courseMetadata.courseId;
axiosMock.onGet(`${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/${courseId}`).reply(200, courseMetadata);
-
history.push(`/course/${courseId}/dates`); // so tab can pull course id from url
});
diff --git a/src/course-home/outline-tab/DateSummary.jsx b/src/course-home/outline-tab/DateSummary.jsx
index acc36cd4..a0d36480 100644
--- a/src/course-home/outline-tab/DateSummary.jsx
+++ b/src/course-home/outline-tab/DateSummary.jsx
@@ -35,11 +35,10 @@ export default function DateSummary({
&& {dateBlock.title}
}
{dateBlock.description
- && {dateBlock.description}
}
+ && {dateBlock.description}
}
{!linkedTitle && dateBlock.link
&& {dateBlock.linkText}}
-
);
}
diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx
index 9a1b0637..c75e4da8 100644
--- a/src/course-home/outline-tab/OutlineTab.test.jsx
+++ b/src/course-home/outline-tab/OutlineTab.test.jsx
@@ -1,107 +1,342 @@
import React from 'react';
import { Factory } from 'rosie';
import { getConfig } from '@edx/frontend-platform';
-import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
-import OutlineTab from './OutlineTab';
+import MockAdapter from 'axios-mock-adapter';
+import userEvent from '@testing-library/user-event';
+
+import { ALERT_TYPES } from '../../generic/user-messages';
+import buildSimpleCourseBlocks from '../data/__factories__/courseBlocks.factory';
import {
- fireEvent, initializeTestStore, logUnhandledRequests, render, screen, waitFor,
+ fireEvent, initializeMockApp, logUnhandledRequests, render, screen, waitFor,
} from '../../setupTest';
import executeThunk from '../../utils';
import * as thunks from '../data/thunks';
-import { ALERT_TYPES } from '../../generic/user-messages';
+import initializeStore from '../../store';
+import OutlineTab from './OutlineTab';
+initializeMockApp();
jest.mock('@edx/frontend-platform/analytics');
describe('Outline Tab', () => {
- let store;
let axiosMock;
- const courseMetadata = Factory.build('courseMetadata');
- const courseHomeMetadata = Factory.build(
- 'courseHomeMetadata', {
- course_id: courseMetadata.id,
- },
- { courseTabs: courseMetadata.tabs },
- );
- const outlineTabData = Factory.build('outlineTabData', {
- courseId: courseMetadata.id,
- resume_course: {
- has_visited_course: false,
- url: `${getConfig().LMS_BASE_URL}/courses/${courseMetadata.id}/jump_to/block-v1:edX+Test+Block@12345abcde`,
- },
- });
- const outlineUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/*`);
const courseMetadataUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/v1/course_metadata/*`);
+ const goalUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/v1/save_course_goal`);
+ const outlineUrl = new RegExp(`${getConfig().LMS_BASE_URL}/api/course_home/v1/outline/*`);
+
+ const store = initializeStore();
+
+ const courseMetadata = Factory.build('courseHomeMetadata');
+ const { courseId } = courseMetadata;
+ const outlineTabData = Factory.build('outlineTabData');
beforeEach(async () => {
- store = await initializeTestStore({ excludeFetchCourse: true, excludeFetchSequence: true, courseMetadata });
-
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
axiosMock.onGet(outlineUrl).reply(200, outlineTabData);
- axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata);
+ axiosMock.onPost(goalUrl).reply(200, { header: 'Success' });
logUnhandledRequests(axiosMock);
- await executeThunk(thunks.fetchOutlineTab(courseMetadata.id), store.dispatch);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
});
- it('displays link to start course', () => {
- render();
- expect(screen.getByRole('link', { name: 'Start Course' })).toBeInTheDocument();
- });
-
- it('displays link to resume course', async () => {
- const outlineTabDataHasVisited = Factory.build('outlineTabData', {
- courseId: courseMetadata.id,
- resume_course: {
- has_visited_course: true,
- url: `${getConfig().LMS_BASE_URL}/courses/${courseMetadata.id}/jump_to/block-v1:edX+Test+Block@12345abcde`,
- },
+ describe('Course Outline', () => {
+ it('displays link to start course', () => {
+ render(, { store });
+ expect(screen.getByRole('link', { name: 'Start Course' })).toBeInTheDocument();
});
- axiosMock.onGet(outlineUrl).reply(200, outlineTabDataHasVisited);
- await executeThunk(thunks.fetchOutlineTab(courseMetadata.id), store.dispatch);
- render();
+ it('displays link to resume course', async () => {
+ const outlineTabDataHasVisited = Factory.build('outlineTabData', {
+ courseId,
+ resume_course: {
+ has_visited_course: true,
+ url: `${getConfig().LMS_BASE_URL}/courses/${courseId}/jump_to/block-v1:edX+Test+Block@12345abcde`,
+ },
+ });
+ axiosMock.onGet(outlineUrl).reply(200, outlineTabDataHasVisited);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
- expect(screen.getByRole('link', { name: 'Resume Course' })).toBeInTheDocument();
+ render(, { store });
+
+ expect(screen.getByRole('link', { name: 'Resume Course' })).toBeInTheDocument();
+ });
+
+ it('expands section that contains resume block', async () => {
+ const { courseBlocks } = await buildSimpleCourseBlocks(courseId, outlineTabData.title, { resumeBlock: true });
+ const outlineTabDataResumeBlock = Factory.build('outlineTabData', {
+ courseId,
+ course_blocks: { blocks: courseBlocks.blocks },
+ });
+ axiosMock.onGet(outlineUrl).reply(200, outlineTabDataResumeBlock);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
+
+ render(, { store });
+ const expandedSectionNode = screen.getByRole('button', { name: /Title of Section/ });
+ expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ it('handles expand/collapse all button click', () => {
+ render(, { store });
+ // Button renders as "Expand All"
+ const expandButton = screen.getByRole('button', { name: 'Expand All' });
+ expect(expandButton).toBeInTheDocument();
+
+ // Section initially renders collapsed
+ const collapsedSectionNode = screen.getByRole('button', { name: /section/ });
+ expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
+
+ // Click to expand section
+ userEvent.click(expandButton);
+ expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'true');
+
+ // Click to collapse section
+ userEvent.click(expandButton);
+ expect(collapsedSectionNode).toHaveAttribute('aria-expanded', 'false');
+ });
+
+ it('displays correct icon for complete assignment', async () => {
+ const { courseBlocks } = await buildSimpleCourseBlocks(courseId, outlineTabData.title, { complete: true });
+ const outlineTabDataCompleteAssignment = Factory.build('outlineTabData', {
+ courseId,
+ course_blocks: { blocks: courseBlocks.blocks },
+ });
+ axiosMock.onGet(outlineUrl).reply(200, outlineTabDataCompleteAssignment);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
+
+ render(, { store });
+ expect(screen.getByTitle('Completed section')).toBeInTheDocument();
+ });
+
+ it('displays correct icon for incomplete assignment', async () => {
+ const { courseBlocks } = await buildSimpleCourseBlocks(courseId, outlineTabData.title, { complete: false });
+ const outlineTabDataIncompleteAssignment = Factory.build('outlineTabData', {
+ courseId,
+ course_blocks: { blocks: courseBlocks.blocks },
+ });
+ axiosMock.onGet(outlineUrl).reply(200, outlineTabDataIncompleteAssignment);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
+
+ render(, { store });
+ expect(screen.getByTitle('Incomplete section')).toBeInTheDocument();
+ });
+ });
+
+ describe('Welcome Message', () => {
+ it('does not render show more/less button under 200 characters', () => {
+ render(, { store });
+ expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: 'Show more' })).not.toBeInTheDocument();
+ });
+
+ describe('over 200 characters', () => {
+ beforeEach(async () => {
+ const outlineTabDataLongMessage = Factory.build('outlineTabData', {
+ courseId,
+ welcome_message_html: ''
+ + 'Welcome to Demonstration Course!!! This message is over 200 characters long. We would like to test the '
+ + 'shorten message feature! When the page renders, this text should be shortened because it is very long.'
+ + '
',
+ });
+ axiosMock.onGet(outlineUrl).reply(200, outlineTabDataLongMessage);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
+
+ render(, { store });
+ });
+
+ it('shortens message', async () => {
+ expect(screen.getByTestId('short-welcome-message-iframe')).toBeInTheDocument();
+ const showMoreButton = screen.queryByRole('button', { name: 'Show More' });
+ expect(showMoreButton).toBeInTheDocument();
+ });
+
+ it('renders show more/less button and handles click', async () => {
+ expect(screen.getByTestId('alert-container-welcome')).toBeInTheDocument();
+ let showMoreButton = screen.getByRole('button', { name: 'Show More' });
+ expect(showMoreButton).toBeInTheDocument();
+
+ userEvent.click(showMoreButton);
+ let showLessButton = screen.getByRole('button', { name: 'Show Less' });
+ expect(showLessButton).toBeInTheDocument();
+ expect(screen.getByTestId('long-welcome-message-iframe')).toBeInTheDocument();
+
+ userEvent.click(showLessButton);
+ showLessButton = screen.queryByRole('button', { name: 'Show Less' });
+ expect(showLessButton).not.toBeInTheDocument();
+ showMoreButton = screen.getByRole('button', { name: 'Show More' });
+ expect(showMoreButton).toBeInTheDocument();
+ });
+ });
+
+ it('does not display if no update available', async () => {
+ const outlineTabDataSansUpdate = Factory.build('outlineTabData', {
+ courseId,
+ welcome_message_html: null,
+ });
+ axiosMock.onGet(outlineUrl).reply(200, outlineTabDataSansUpdate);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
+
+ render(, { store });
+ expect(screen.queryByTestId('alert-container-welcome')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Course Goals', () => {
+ const goalOptions = [
+ ['certify', 'Earn a certificate'],
+ ['complete', 'Complete the course'],
+ ['explore', 'Explore the course'],
+ ['unsure', 'Not sure yet'],
+ ];
+
+ it('does not render goal widgets if no goals available', () => {
+ render(, { store });
+ expect(screen.queryByTestId('course-goal-card')).not.toBeInTheDocument();
+ expect(screen.queryByLabelText('Goal')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('edit-goal-selector')).not.toBeInTheDocument();
+ });
+
+ describe('goal is not set', () => {
+ beforeEach(async () => {
+ const outlineTabDataGoalNotSet = Factory.build('outlineTabData', {
+ courseId,
+ course_goals: {
+ goal_options: goalOptions,
+ selected_goal: null,
+ },
+ });
+ axiosMock.onGet(outlineUrl).reply(200, outlineTabDataGoalNotSet);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
+
+ render(, { store });
+ });
+
+ it('renders goal card', () => {
+ expect(screen.queryByLabelText('Goal')).not.toBeInTheDocument();
+ expect(screen.getByTestId('course-goal-card')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Earn a certificate' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Complete the course' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Explore the course' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Not sure yet' })).toBeInTheDocument();
+ });
+
+ it('renders goal selector on goal selection', async () => {
+ const certifyGoalButton = screen.getByRole('button', { name: 'Earn a certificate' });
+ fireEvent.click(certifyGoalButton);
+
+ const goalSelector = await screen.findByTestId('edit-goal-selector');
+ expect(goalSelector).toBeInTheDocument();
+ });
+ });
+
+ describe('goal is set', () => {
+ beforeEach(async () => {
+ const outlineTabDataGoalSet = Factory.build('outlineTabData', {
+ courseId,
+ course_goals: {
+ goal_options: goalOptions,
+ selected_goal: { text: 'Earn a certificate', key: 'certify' },
+ },
+ });
+
+ axiosMock.onGet(outlineUrl).reply(200, outlineTabDataGoalSet);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
+
+ render(, { store });
+ });
+
+ it('renders edit goal selector', () => {
+ expect(screen.getByLabelText('Goal')).toBeInTheDocument();
+ expect(screen.getByTestId('edit-goal-selector')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Earn a certificate' })).toBeInTheDocument();
+ });
+
+ it('updates goal on click', async () => {
+ // Open dropdown
+ const dropdownButtonNode = screen.getByRole('button', { name: 'Earn a certificate' });
+ await waitFor(() => {
+ expect(dropdownButtonNode).toBeInTheDocument();
+ });
+ fireEvent.click(dropdownButtonNode);
+
+ // Select a new goal
+ const unsureButtonNode = screen.getByRole('button', { name: 'Not sure yet' });
+ await waitFor(() => {
+ expect(unsureButtonNode).toBeInTheDocument();
+ });
+ fireEvent.click(unsureButtonNode);
+
+ // Verify the request was made
+ await waitFor(() => {
+ expect(axiosMock.history.post[0].url).toMatch(goalUrl);
+ expect(axiosMock.history.post[0].data).toMatch(`{"course_id":"${courseId}","goal_key":"unsure"}`);
+ });
+ });
+ });
+ });
+
+ describe('Course Handouts', () => {
+ it('renders title when handouts are available', () => {
+ render(, { store });
+ expect(screen.queryByRole('heading', { name: 'Course Handouts' })).toBeInTheDocument();
+ });
+
+ it('does not display title if no handouts available', async () => {
+ const outlineTabDataSansHandout = Factory.build('outlineTabData', {
+ courseId,
+ handouts_html: null,
+ });
+ axiosMock.onGet(outlineUrl).reply(200, outlineTabDataSansHandout);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
+
+ render(, { store });
+ expect(screen.queryByRole('heading', { name: 'Course Handouts' })).not.toBeInTheDocument();
+ });
});
describe('Alert List', () => {
describe('Enrollment Alert', () => {
- const extraText = outlineTabData.enroll_alert.extra_text;
- const alertMessage = `You must be enrolled in the course to see course content. ${extraText}`;
- const staffMessage = 'You are viewing this course as staff, and are not enrolled.';
+ let extraText;
+ let alertMessage;
+ let staffMessage;
+
+ beforeEach(() => {
+ extraText = outlineTabData.enroll_alert.extra_text;
+ alertMessage = `You must be enrolled in the course to see course content. ${extraText}`;
+ staffMessage = 'You are viewing this course as staff, and are not enrolled.';
+ });
it('does not display enrollment alert for enrolled user', async () => {
const courseHomeMetadataForEnrolledUser = Factory.build(
- 'courseHomeMetadata', { course_id: courseMetadata.id, is_enrolled: true },
+ 'courseHomeMetadata', { course_id: courseId, is_enrolled: true },
{ courseTabs: courseMetadata.tabs },
);
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadataForEnrolledUser);
- await executeThunk(thunks.fetchOutlineTab(courseMetadata.id), store.dispatch);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
- render();
+ render(, { store });
expect(screen.queryByText(alertMessage)).not.toBeInTheDocument();
});
it('does not display enrollment button if enrollment is not available', async () => {
const outlineTabDataCannotEnroll = Factory.build('outlineTabData', {
- courseId: courseMetadata.id,
+ courseId,
enroll_alert: {
can_enroll: false,
extra_text: extraText,
},
});
axiosMock.onGet(outlineUrl).reply(200, outlineTabDataCannotEnroll);
- await executeThunk(thunks.fetchOutlineTab(courseMetadata.id), store.dispatch);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
- render();
+ render(, { store });
expect(screen.queryByRole('button', { name: 'Enroll Now' })).not.toBeInTheDocument();
});
it('displays enrollment alert for unenrolled user', async () => {
- render();
+ render(, { store });
const alert = await screen.findByText(alertMessage);
expect(alert).toHaveAttribute('role', 'alert');
@@ -113,7 +348,7 @@ describe('Outline Tab', () => {
it('displays different message for unenrolled staff user', async () => {
const courseHomeMetadataForUnenrolledStaff = Factory.build(
- 'courseHomeMetadata', { course_id: courseMetadata.id, is_staff: true },
+ 'courseHomeMetadata', { course_id: courseId, is_staff: true },
{ courseTabs: courseMetadata.tabs },
);
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadataForUnenrolledStaff);
@@ -121,14 +356,14 @@ describe('Outline Tab', () => {
// show, which makes this test easier to write. If there's only one, it's easy to query
// for below.
const outlineTabDataCannotEnroll = Factory.build('outlineTabData', {
- courseId: courseMetadata.id,
+ courseId,
offer_html: null,
course_expired_html: null,
});
axiosMock.onGet(outlineUrl).reply(200, outlineTabDataCannotEnroll);
- await executeThunk(thunks.fetchOutlineTab(courseMetadata.id), store.dispatch);
+ await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
- render();
+ render(, { store });
const alert = await screen.findByText(staffMessage);
expect(alert).toHaveAttribute('role', 'alert');
@@ -146,13 +381,13 @@ describe('Outline Tab', () => {
window.location = {
reload: jest.fn(),
};
- render();
+ render(, { store });
const button = await screen.findByRole('button', { name: 'Enroll Now' });
fireEvent.click(button);
await waitFor(() => expect(axiosMock.history.post).toHaveLength(1));
expect(axiosMock.history.post[0].data)
- .toEqual(JSON.stringify({ course_details: { course_id: courseMetadata.id } }));
+ .toEqual(JSON.stringify({ course_details: { course_id: courseId } }));
expect(window.location.reload).toHaveBeenCalledTimes(1);
window.location = location;
diff --git a/src/course-home/outline-tab/widgets/CourseGoalCard.jsx b/src/course-home/outline-tab/widgets/CourseGoalCard.jsx
index 7c068423..1247b9b0 100644
--- a/src/course-home/outline-tab/widgets/CourseGoalCard.jsx
+++ b/src/course-home/outline-tab/widgets/CourseGoalCard.jsx
@@ -34,7 +34,7 @@ function CourseGoalCard({
}
return (
-
+
diff --git a/src/course-home/outline-tab/widgets/UpdateGoalSelector.jsx b/src/course-home/outline-tab/widgets/UpdateGoalSelector.jsx
index 8d27dba5..5134d718 100644
--- a/src/course-home/outline-tab/widgets/UpdateGoalSelector.jsx
+++ b/src/course-home/outline-tab/widgets/UpdateGoalSelector.jsx
@@ -45,7 +45,7 @@ function UpdateGoalSelector({
-
+
{selectedGoal.text}
diff --git a/src/course-home/outline-tab/widgets/WelcomeMessage.jsx b/src/course-home/outline-tab/widgets/WelcomeMessage.jsx
index 9b62b5a6..5bb8977c 100644
--- a/src/course-home/outline-tab/widgets/WelcomeMessage.jsx
+++ b/src/course-home/outline-tab/widgets/WelcomeMessage.jsx
@@ -53,12 +53,14 @@ function WelcomeMessage({ courseId, intl }) {
{showShortMessage ? (
) : (