Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Draft
- Fall back to the store's default backorder message on PDP when a product has no explicit message assignment or references a deleted message [#2708](https://github.com/bigcommerce/cornerstone/pull/2708)
- Decouple "Show backorder message" from "Qty on Backorder" settings so each works independently on PDP [#2701](https://github.com/bigcommerce/cornerstone/pull/2701)

## 6.20.0 (07-02-2026)
Expand Down
66 changes: 66 additions & 0 deletions assets/js/test-unit/theme/common/picklist-backorder.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,72 @@ describe('PicklistBackorder', () => {
expect($items.eq(0).text()).not.toContain('|');
});

it('falls back to the default message when backorder_message_id is null', () => {
$scope = buildScope(oneAttr('Bundle 1', 98, 'opt'));
const renderer = new PicklistBackorder($scope, {
...context,
backorderMessages: [
{ id: 1, message: 'Ships in 2 weeks', is_default: true },
{ id: 2, message: 'Backorder message 1' },
],
});

renderer.render({
selected_picklist_options: [selection()],
picklist_products_details: [detail({
available_on_hand: 0,
available_for_backorder: 10,
backorder_message_id: null,
})],
}, 5);

const $items = $('[data-picklist-backorder-list] li', $scope);
expect($items.length).toBe(1);
expect($items.eq(0).text()).toContain('Ships in 2 weeks');
});

it('falls back to the default message when backorder_message_id is a dangling reference', () => {
$scope = buildScope(oneAttr('Bundle 1', 98, 'opt'));
const renderer = new PicklistBackorder($scope, {
...context,
backorderMessages: [
{ id: 1, message: 'Ships in 2 weeks', is_default: true },
{ id: 2, message: 'Backorder message 1' },
],
});

renderer.render({
selected_picklist_options: [selection()],
picklist_products_details: [detail({
available_on_hand: 0,
available_for_backorder: 10,
backorder_message_id: 9999,
})],
}, 5);

const $items = $('[data-picklist-backorder-list] li', $scope);
expect($items.length).toBe(1);
expect($items.eq(0).text()).toContain('Ships in 2 weeks');
});

it('shows nothing when backorder_message_id is null and the default message is blank', () => {
$scope = buildScope(oneAttr('Bundle 1', 98, 'opt'));
const renderer = new PicklistBackorder($scope, context);

renderer.render({
selected_picklist_options: [selection()],
picklist_products_details: [detail({
available_on_hand: 0,
available_for_backorder: 10,
backorder_message_id: null,
})],
}, 5);

const $items = $('[data-picklist-backorder-list] li', $scope);
expect($items.length).toBe(1);
expect($items.eq(0).text()).not.toContain('|');
});

it('omits the message suffix when context.backorderMessages is missing', () => {
$scope = buildScope(oneAttr('Bundle 1', 98, 'opt'));
const renderer = new PicklistBackorder(
Expand Down
118 changes: 118 additions & 0 deletions assets/js/test-unit/theme/common/product-details-base.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,124 @@ describe('ProductDetailsBase.updateAddToCartForQty()', () => {
});
});

describe('ProductDetailsBase.updateBackorderMessage()', () => {
let $scope;

const backorderMessages = [
{ id: 1, message: 'Ships in 2 weeks', is_default: true },
{ id: 2, message: 'Custom delay notice' },
];

const buildBackorderScope = () => $(`
<div>
<div data-product-backordered>
<span data-backorder-message></span>
</div>
</div>
`).appendTo(document.body);

const makeBackorderInstance = (context, backorderedQty) => {
const obj = Object.create(ProductDetailsBase.prototype);
obj.$scope = $scope;
obj.context = context;
obj.backorderedQty = backorderedQty;
return obj;
};

const backorderViewModel = () => ({
$backordered: $('[data-product-backordered]', $scope),
$backorderMessage: $('[data-backorder-message]', $scope),
});

afterEach(() => {
if ($scope) {
$scope.remove();
$scope = null;
}
});

it('shows the assigned message when backorder_message_id matches', () => {
$scope = buildBackorderScope();
const instance = makeBackorderInstance({
showBackorderMessage: true,
backorderMessages,
backorderMessageId: 2,
}, 5);

instance.updateBackorderMessage(backorderViewModel());

expect($('[data-backorder-message]', $scope).text()).toBe('Custom delay notice');
});

it('falls back to the default message when backorder_message_id is null', () => {
$scope = buildBackorderScope();
const instance = makeBackorderInstance({
showBackorderMessage: true,
backorderMessages,
backorderMessageId: null,
}, 5);

instance.updateBackorderMessage(backorderViewModel());

expect($('[data-backorder-message]', $scope).text()).toBe('Ships in 2 weeks');
});

it('falls back to the default message when backorder_message_id is a dangling reference', () => {
$scope = buildBackorderScope();
const instance = makeBackorderInstance({
showBackorderMessage: true,
backorderMessages,
backorderMessageId: 9999,
}, 5);

instance.updateBackorderMessage(backorderViewModel());

expect($('[data-backorder-message]', $scope).text()).toBe('Ships in 2 weeks');
});

it('shows nothing when backorder_message_id is null and the default message is blank', () => {
$scope = buildBackorderScope();
const instance = makeBackorderInstance({
showBackorderMessage: true,
backorderMessages: [
{ id: 1, message: '', is_default: true },
{ id: 2, message: 'Custom delay notice' },
],
backorderMessageId: null,
}, 5);

instance.updateBackorderMessage(backorderViewModel());

expect($('[data-backorder-message]', $scope).text()).toBe('');
});

it('shows nothing when showBackorderMessage is false', () => {
$scope = buildBackorderScope();
const instance = makeBackorderInstance({
showBackorderMessage: false,
backorderMessages,
backorderMessageId: 2,
}, 5);

instance.updateBackorderMessage(backorderViewModel());

expect($('[data-backorder-message]', $scope).text()).toBe('');
});

it('clears the message when backorderedQty is zero', () => {
$scope = buildBackorderScope();
const instance = makeBackorderInstance({
showBackorderMessage: true,
backorderMessages,
backorderMessageId: 2,
}, 0);

instance.updateBackorderMessage(backorderViewModel());

expect($('[data-backorder-message]', $scope).text()).toBe('');
});
});

describe('ProductDetailsBase.reselectHiddenSelectedValues', () => {
let $scope;

Expand Down
5 changes: 3 additions & 2 deletions assets/js/theme/common/picklist-backorder.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { findByBackorderMessageIdOrDefault } from './utils/backorder-utils';

export default class PicklistBackorder {
constructor($scope, context) {
this.$scope = $scope;
Expand Down Expand Up @@ -172,13 +174,12 @@ export default class PicklistBackorder {
if (!detail) return '';

const messageId = detail.backorder_message_id;
if (messageId == null) return '';

const { backorderMessages, showBackorderMessage } = this.context;
if (!showBackorderMessage) return '';
if (!Array.isArray(backorderMessages)) return '';

const messageObj = backorderMessages.find(m => m.id === messageId);
const messageObj = findByBackorderMessageIdOrDefault(backorderMessages, messageId);
return messageObj && messageObj.message ? messageObj.message : '';
}
}
5 changes: 3 additions & 2 deletions assets/js/theme/common/product-details-base.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Wishlist from '../wishlist';
import { initRadioOptions } from './aria';
import PicklistBackorder from './picklist-backorder';
import { findByBackorderMessageIdOrDefault } from './utils/backorder-utils';

const optionsTypesMap = {
INPUT_FILE: 'input-file',
Expand Down Expand Up @@ -471,8 +472,8 @@ export default class ProductDetailsBase {

const { showBackorderMessage, backorderMessages, backorderMessageId } = this.context;

if (showBackorderMessage && backorderMessageId != null && Array.isArray(backorderMessages)) {
const messageObj = backorderMessages.find(m => m.id === backorderMessageId);
if (showBackorderMessage && Array.isArray(backorderMessages)) {
const messageObj = findByBackorderMessageIdOrDefault(backorderMessages, backorderMessageId);
if (messageObj) {
viewModel.$backorderMessage.text(messageObj.message);
return;
Expand Down
6 changes: 6 additions & 0 deletions assets/js/theme/common/utils/backorder-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function findByBackorderMessageIdOrDefault(messages, id) {
const assignedMessage = messages.find(m => m.id === id);
const defaultMessage = messages.find(m => m.is_default);

return assignedMessage ?? defaultMessage;
}
Loading