Răsfoiți Sursa

test(email-plugin): Add some tests

Relates to #88
Michael Bromley 6 ani în urmă
părinte
comite
d63007df9e

+ 2 - 1
package.json

@@ -19,9 +19,10 @@
     "prepush": "yarn test:all && cd admin-ui && yarn build --prod",
     "dev-server": "cd packages/dev-server && yarn start",
     "dev-server:populate": "cd packages/dev-server && yarn populate",
-    "test:all": "cd admin-ui && yarn test --watch=false --browsers=ChromeHeadlessCI --progress=false && cd ../ && yarn test:common && yarn test:core && yarn test:e2e",
+    "test:all": "cd admin-ui && yarn test --watch=false --browsers=ChromeHeadlessCI --progress=false && cd ../ && yarn test:common && yarn test:core && yarn test:email-plugin && yarn test:e2e",
     "test:common": "jest --config packages/common/jest.config.js",
     "test:core": "jest --config packages/core/jest.config.js",
+    "test:email-plugin": "jest --config packages/email-plugin/jest.config.js",
     "test:e2e": "jest --config packages/core/e2e/config/jest-e2e.json --runInBand",
     "test:admin-ui": "cd admin-ui && yarn test --watch=false --browsers=ChromeHeadlessCI --progress=false",
     "build": "lerna run build",

+ 0 - 1
packages/core/package.json

@@ -17,7 +17,6 @@
   "license": "MIT",
   "scripts": {
     "lint": "tslint --project tsconfig.json -c tslint.json",
-    "generate-email-preview": "node -r ts-node/register src/email/preview/generate-email-preview.ts",
     "tsc:watch": "tsc -p ./build/tsconfig.build.json --watch",
     "gulp:watch": "gulp -f ./build/gulpfile.ts watch",
     "build": "rimraf dist && tsc -p ./build/tsconfig.build.json && tsc -p ./build/tsconfig.cli.json && gulp -f ./build/gulpfile.ts build",

+ 23 - 0
packages/email-plugin/jest.config.js

@@ -0,0 +1,23 @@
+module.exports = {
+    coverageDirectory: 'coverage',
+    moduleFileExtensions: [
+        'js',
+        'json',
+        'ts',
+    ],
+    preset: 'ts-jest',
+    rootDir: __dirname,
+    roots: [
+        '<rootDir>/src',
+    ],
+    transform: {
+        '^.+\\.(t|j)s$': 'ts-jest',
+    },
+    globals: {
+        'ts-jest': {
+            tsConfig: {
+                allowJs: true,
+            },
+        },
+    },
+};

+ 15 - 21
packages/email-plugin/src/default-email-handlers.ts

@@ -6,33 +6,27 @@ import { EmailEventListener } from './event-listener';
 export const orderConfirmationHandler = new EmailEventListener('order-confirmation')
     .on(OrderStateTransitionEvent)
     .filter(event => event.toState === 'PaymentSettled' && !!event.order.customer)
-    .configure({
-        setRecipient: event => event.order.customer!.emailAddress,
-        subject: `Order confirmation for #{{ order.code }}`,
-        templateVars: event => ({ order: event.order }),
-    });
+    .setRecipient(event => event.order.customer!.emailAddress)
+    .setSubject(`Order confirmation for #{{ order.code }}`)
+    .setTemplateVars(event => ({ order: event.order }));
 
 export const emailVerificationHandler = new EmailEventListener('email-verification')
     .on(AccountRegistrationEvent)
-    .configure({
-        setRecipient: event => event.user.identifier,
-        subject: `Please verify your email address`,
-        templateVars: event => ({
-            user: event.user,
-            verifyUrl: 'verify',
-        }),
-    });
+    .setRecipient(event => event.user.identifier)
+    .setSubject(`Please verify your email address`)
+    .setTemplateVars(event => ({
+        user: event.user,
+        verifyUrl: 'verify',
+    }));
 
 export const passwordResetHandler = new EmailEventListener('password-reset')
     .on(PasswordResetEvent)
-    .configure({
-        setRecipient: event => event.user.identifier,
-        subject: `Forgotten password reset`,
-        templateVars: event => ({
-            user: event.user,
-            passwordResetUrl: 'reset-password',
-        }),
-    });
+    .setRecipient(event => event.user.identifier)
+    .setSubject(`Forgotten password reset`)
+    .setTemplateVars(event => ({
+        user: event.user,
+        passwordResetUrl: 'reset-password',
+    }));
 
 export const defaultEmailHandlers = [
     orderConfirmationHandler,

+ 148 - 23
packages/email-plugin/src/event-listener.ts

@@ -1,30 +1,108 @@
 import { Type } from '@vendure/common/lib/shared-types';
 import { LanguageCode } from '@vendure/common/src/generated-types';
 
-import { EventWithContext } from './types';
+import { EmailDetails, EventWithContext } from './types';
 
-export interface EmailEventHandlerConfig<T extends string, Event extends EventWithContext> {
-    channelCode?: string;
-    languageCode?: LanguageCode;
-    setRecipient: (event: Event) => string;
+/**
+ * @description
+ * Configures the {@link EmailEventHandler} to handle a particular channel & languageCode
+ * combination.
+ *
+ * @docsCategory EmailPlugin
+ */
+export interface EmailTemplateConfig {
+    /**
+     * @description
+     * Specifies the channel to which this configuration will apply. If set to `'default'`, it will be applied to all
+     * channels.
+     */
+    channelCode: string | 'default';
+    /**
+     * @description
+     * Specifies the languageCode to which this configuration will apply. If set to `'default'`, it will be applied to all
+     * languages.
+     */
+    languageCode: LanguageCode | 'default';
+    /**
+     * @description
+     * Defines the file name of the Handlebars template file to be used to when generating this email.
+     */
+    templateFile: string;
+    /**
+     * @description
+     * A string defining the email subject line. Handlebars variables defined in the `templateVars` object may
+     * be used inside the subject.
+     */
     subject: string;
-    templateVars: (event: Event) => { [key: string]: any; };
 }
 
+/**
+ * @description
+ * An EmailEventListener is used to listen for events and set up a {@link EmailEventHandler} which
+ * defines how an email will be generated from this event.
+ *
+ * @docsCategory EmailPlugin
+ */
 export class EmailEventListener<T extends string> {
     public type: T;
     constructor(type: T) {
         this.type = type;
     }
 
-    on<Event extends EventWithContext>(event: Type<Event>) {
+    /**
+     * @description
+     * Defines the event to listen for.
+     */
+    on<Event extends EventWithContext>(event: Type<Event>): EmailEventHandler<T, Event> {
         return new EmailEventHandler<T, Event>(this, event);
     }
 }
 
+/**
+ * @description
+ * The EmailEventHandler defines how the EmailPlugin will respond to a given event.
+ *
+ * A handler is created by creating a new {@link EmailEventListener} and calling the `.on()` method
+ * to specify which event to respond to.
+ *
+ * @example
+ * ```ts
+ * const confirmationHandler = new EmailEventListener('order-confirmation')
+ *   .on(OrderStateTransitionEvent)
+ *   .filter(event => event.toState === 'PaymentSettled')
+ *   .setRecipient(event => event.order.customer.emailAddress)
+ *   .setSubject(`Order confirmation for #{{ order.code }}`)
+ *   .setTemplateVars(event => ({ order: event.order }));
+ * ```
+ *
+ * This example creates a handler which listens for the `OrderStateTransitionEvent` and if the Order has
+ * transitioned to the `'PaymentSettled'` state, it will generate and send an email.
+ *
+ * ## Handling other languages
+ *
+ * By default, the handler will respond to all events on all channels and use the same subject ("Order confirmation for #12345" above)
+ * and body template. Where the server is intended to support multiple languages, the `.addTemplate()` method may be used
+ * to defined the subject and body template for specific language and channel combinations.
+ *
+ * @example
+ * ```ts
+ * const extendedConfirmationHandler = confirmationHandler
+ *   .addTemplate({
+ *     channelCode: 'default',
+ *     languageCode: LanguageCode.de,
+ *     templateFile: 'body.de.hbs',
+ *     subject: 'Bestellbestätigung für #{{ order.code }}',
+ *   })
+ * ```
+ *
+ * @docsCategory EmailPlugin
+ */
 export class EmailEventHandler<T extends string = string, Event extends EventWithContext = EventWithContext> {
+    private setRecipientFn: (event: Event) => string;
+    private setTemplateVarsFn: (event: Event) => { [key: string]: any; };
     private filterFns: Array<(event: Event) => boolean> = [];
-    private configurations: Array<EmailEventHandlerConfig<T, Event>> = [];
+    private configurations: EmailTemplateConfig[] = [];
+    private defaultSubject: string;
 
     constructor(public listener: EmailEventListener<T>, public event: Type<Event>) {}
 
@@ -32,48 +110,95 @@ export class EmailEventHandler<T extends string = string, Event extends EventWit
         return this.listener.type;
     }
 
-    filter(filterFn: (event: Event) => boolean) {
+    /**
+     * @description
+     * Defines a predicate function which is used to determine whether the event will trigger an email.
+     * Multiple filter functions may be defined.
+     */
+    filter(filterFn: (event: Event) => boolean): EmailEventHandler<T, Event> {
         this.filterFns.push(filterFn);
         return this;
     }
 
-    configure(templateConfig: EmailEventHandlerConfig<T, Event>) {
-        this.configurations.push(templateConfig);
+    /**
+     * @description
+     * A function which defines how the recipient email address should be extracted from the incoming event.
+     */
+    setRecipient(setRecipientFn: (event: Event) => string): EmailEventHandler<T, Event> {
+        this.setRecipientFn = setRecipientFn;
         return this;
     }
 
-    handle(event: Event) {
+    /**
+     * @description
+     * A function which returns an object hash of variables which will be made available to the Handlebars template
+     * and subject line for interpolation.
+     */
+    setTemplateVars(templateVarsFn: (event: Event) => { [key: string]: any; }): EmailEventHandler<T, Event> {
+        this.setTemplateVarsFn = templateVarsFn;
+        return this;
+    }
+
+    /**
+     * @description
+     * Sets the default subject of the email. The subject string may use Handlebars variables defined by the
+     * setTemplateVars() method.
+     */
+    setSubject(defaultSubject: string): EmailEventHandler<T, Event> {
+        this.defaultSubject = defaultSubject;
+        return this;
+    }
+
+    /**
+     * @description
+     * Add configuration for another template other than the default `"body.hbs"`. Use this method to define specific
+     * templates for channels or languageCodes other than the default.
+     */
+    addTemplate(config: EmailTemplateConfig): EmailEventHandler<T, Event> {
+        this.configurations.push(config);
+        return this;
+    }
+
+    /**
+     * @description
+     * Used internally by the EmailPlugin to handle incoming events.
+     */
+    handle(event: Event): { recipient: string; templateVars: any; subject: string; templateFile: string; } | undefined {
         for (const filterFn of this.filterFns) {
             if (!filterFn(event)) {
                 return;
             }
         }
+        if (!this.setRecipientFn) {
+            throw new Error(`No setRecipientFn has been defined. ` +
+            `Remember to call ".setRecipient()" when setting up the EmailEventHandler for ${this.type}`);
+        }
         const { ctx } = event;
         const configuration = this.getBestConfiguration(ctx.channel.code, ctx.languageCode);
-        const recipient = configuration.setRecipient(event);
-        const templateVars = configuration.templateVars(event);
+        const recipient = this.setRecipientFn(event);
+        const templateVars = this.setTemplateVarsFn ? this.setTemplateVarsFn(event) : {};
         return {
             recipient,
             templateVars,
-            subject: configuration.subject,
+            subject: configuration ? configuration.subject : this.defaultSubject,
+            templateFile: configuration ? configuration.templateFile : 'body.hbs',
         };
     }
 
-    private getBestConfiguration(channelCode: string, languageCode: LanguageCode) {
+    private getBestConfiguration(channelCode: string, languageCode: LanguageCode): EmailTemplateConfig | undefined {
         if ( this.configurations.length === 0) {
-            throw new Error(`This handler has not yet been configured`);
-        }
-        if (this.configurations.length === 1) {
-            return this.configurations[0];
+            return;
         }
-        const exactMatch = this.configurations.find(c => c.channelCode === channelCode && c.languageCode === languageCode);
+        const exactMatch = this.configurations.find(c => {
+            return (c.channelCode === channelCode || c.channelCode === 'default') && c.languageCode === languageCode;
+        });
         if (exactMatch) {
             return exactMatch;
         }
-        const channelMatch = this.configurations.find(c => c.channelCode === channelCode && c.languageCode === undefined);
+        const channelMatch = this.configurations.find(c => c.channelCode === channelCode && c.languageCode === 'default');
         if (channelMatch) {
             return channelMatch;
         }
-        return this.configurations[0];
+        return;
     }
 }

+ 2 - 2
packages/email-plugin/src/handlebars-mjml-generator.ts

@@ -22,12 +22,12 @@ export class HandlebarsMjmlGenerator implements EmailGenerator {
         template: string,
         templateContext: any,
     ) {
-        const compiledTemplate = Handlebars.compile(template);
         const compiledSubject = Handlebars.compile(subject);
+        const compiledTemplate = Handlebars.compile(template);
         const subjectResult = compiledSubject(templateContext);
         const mjml = compiledTemplate(templateContext);
         const body = mjml2html(mjml).html;
-        return { subject, body };
+        return { subject: subjectResult, body };
     }
 
     private registerPartials(partialsPath: string) {

+ 210 - 0
packages/email-plugin/src/plugin.spec.ts

@@ -0,0 +1,210 @@
+/* tslint:disable:no-non-null-assertion */
+import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
+import { LanguageCode } from '@vendure/common/src/generated-types';
+import { EventBus, Order, OrderStateTransitionEvent, VendureEvent } from '@vendure/core';
+import path from 'path';
+
+import { orderConfirmationHandler } from './default-email-handlers';
+import { EmailEventHandler, EmailEventListener } from './event-listener';
+import { EmailPlugin } from './plugin';
+
+describe('EmailPlugin', () => {
+    let plugin: EmailPlugin;
+    let eventBus: EventBus;
+    let onSend: jest.Mock;
+
+    async function initPluginWithHandlers(handlers: Array<EmailEventHandler<any, any>>, templatePath?: string) {
+        eventBus = new EventBus();
+        onSend = jest.fn();
+        plugin = new EmailPlugin({
+            templatePath: templatePath || path.join(__dirname, '../test-templates'),
+            transport: {
+                type: 'testing',
+                onSend,
+            },
+            handlers,
+        });
+
+        const inject = (token: any): any => {
+            if (token === EventBus) {
+                return eventBus;
+            } else {
+                throw new Error(`Was not expecting to inject the token ${token}`);
+            }
+        };
+
+        await plugin.onBootstrap(inject);
+    }
+
+    describe('event filtering', () => {
+
+        const ctx = {
+            channel: { code: DEFAULT_CHANNEL_CODE },
+            languageCode: LanguageCode.en,
+        } as any;
+
+        it('single filter', async () => {
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .filter(event => event.shouldSend === true)
+                .setRecipient(() => 'test@test.com')
+                .setSubject('test subject');
+
+            await initPluginWithHandlers([handler]);
+
+            eventBus.publish(new MockEvent(ctx, false));
+            await pause();
+            expect(onSend).not.toHaveBeenCalled();
+
+            eventBus.publish(new MockEvent(ctx, true));
+            await pause();
+            expect(onSend).toHaveBeenCalledTimes(1);
+        });
+
+        it('multiple filters', async () => {
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .filter(event => event.shouldSend === true)
+                .filter(event => !!event.ctx.user)
+                .setRecipient(() => 'test@test.com')
+                .setSubject('test subject');
+
+            await initPluginWithHandlers([handler]);
+
+            eventBus.publish(new MockEvent(ctx, true));
+            await pause();
+            expect(onSend).not.toHaveBeenCalled();
+
+            eventBus.publish(new MockEvent({ ...ctx, user: 'joe' }, true));
+            await pause();
+            expect(onSend).toHaveBeenCalledTimes(1);
+        });
+    });
+
+    describe('templateVars', () => {
+        const ctx = {
+            channel: { code: DEFAULT_CHANNEL_CODE },
+            languageCode: LanguageCode.en,
+        } as any;
+
+        it('interpolates subject', async () => {
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .setRecipient(() => 'test@test.com')
+                .setSubject('Hello {{ subjectVar }}')
+                .setTemplateVars(event => ({ subjectVar: 'foo' }));
+
+            await initPluginWithHandlers([handler]);
+
+            eventBus.publish(new MockEvent(ctx, true));
+            await pause();
+            expect(onSend.mock.calls[0][0].subject).toBe('Hello foo');
+        });
+
+        it('interpolates body', async () => {
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .setRecipient(() => 'test@test.com')
+                .setSubject('Hello')
+                .setTemplateVars(event => ({ testVar: 'this is the test var' }));
+
+            await initPluginWithHandlers([handler]);
+
+            eventBus.publish(new MockEvent(ctx, true));
+            await pause();
+            expect(onSend.mock.calls[0][0].body).toContain('this is the test var');
+        });
+    });
+
+    describe('multiple configs', () => {
+        const ctx = {
+            channel: { code: DEFAULT_CHANNEL_CODE },
+            languageCode: LanguageCode.en,
+        } as any;
+
+        it('additional LanguageCode', async () => {
+            const handler = new EmailEventListener('test')
+                .on(MockEvent)
+                .setSubject('Hello, {{ name }}!')
+                .setRecipient(() => 'test@test.com')
+                .setTemplateVars(() => ({ name: 'Test' }))
+                .addTemplate({
+                    channelCode: 'default',
+                    languageCode: LanguageCode.de,
+                    templateFile: 'body.de.hbs',
+                    subject: 'Servus, {{ name }}!',
+                });
+
+            await initPluginWithHandlers([handler]);
+
+            eventBus.publish(new MockEvent({ ...ctx, languageCode: LanguageCode.ta }, true));
+            await pause();
+            expect(onSend.mock.calls[0][0].subject).toBe('Hello, Test!');
+            expect(onSend.mock.calls[0][0].body).toContain('Default body.');
+
+            eventBus.publish(new MockEvent({ ...ctx, languageCode: LanguageCode.de }, true));
+            await pause();
+            expect(onSend.mock.calls[1][0].subject).toBe('Servus, Test!');
+            expect(onSend.mock.calls[1][0].body).toContain('German body.');
+        });
+    });
+
+    describe('orderConfirmationHandler', () => {
+
+        beforeEach(async () => {
+            await initPluginWithHandlers([orderConfirmationHandler], path.join(__dirname, '../templates'));
+        });
+
+        const ctx = {
+            channel: { code: DEFAULT_CHANNEL_CODE },
+            languageCode: LanguageCode.en,
+        } as any;
+
+        const order = {
+            code: 'ABCDE',
+            customer: {
+                emailAddress: 'test@test.com',
+            },
+        } as Partial<Order> as any;
+
+        it('filters events with wrong order state', async () => {
+            eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'ArrangingPayment', ctx, order));
+            await pause();
+            expect(onSend).not.toHaveBeenCalled();
+
+            eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'Cancelled', ctx, order));
+            await pause();
+            expect(onSend).not.toHaveBeenCalled();
+
+            eventBus.publish(new OrderStateTransitionEvent('AddingItems', 'OrderComplete', ctx, order));
+            await pause();
+            expect(onSend).not.toHaveBeenCalled();
+
+            eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
+            await pause();
+            expect(onSend).toHaveBeenCalledTimes(1);
+        });
+
+        it('sets the Order Customer emailAddress as recipient', async () => {
+            eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
+            await pause();
+
+            expect(onSend.mock.calls[0][0].recipient).toBe(order.customer!.emailAddress);
+        });
+
+        it('sets the subject', async () => {
+            eventBus.publish(new OrderStateTransitionEvent('ArrangingPayment', 'PaymentSettled', ctx, order));
+            await pause();
+
+            expect(onSend.mock.calls[0][0].subject).toBe(`Order confirmation for #${order.code}`);
+        });
+    });
+});
+
+const pause = () => new Promise(resolve => setTimeout(resolve, 10));
+
+class MockEvent extends VendureEvent {
+    constructor(public ctx: any, public shouldSend: boolean) {
+        super();
+    }
+}

+ 9 - 3
packages/email-plugin/src/plugin.ts

@@ -22,12 +22,13 @@ import { EmailPluginDevModeOptions, EmailPluginOptions, EmailTransportOptions, E
  *
  * @example
  * ```ts
- * import { EmailPlugin } from '@vendure/email-plugin';
+ * import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
  *
  * const config: VendureConfig = {
  *   // Add an instance of the plugin to the plugins array
  *   plugins: [
  *     new EmailPlugin({
+ *       handlers: defaultEmailHandlers,
  *       templatePath: path.join(__dirname, 'vendure/email/templates'),
  *       transport: {
  *         type: 'smtp',
@@ -84,6 +85,12 @@ import { EmailPluginDevModeOptions, EmailPluginOptions, EmailTransportOptions, E
  * * `formatMoney`: Formats an amount of money (which are always stored as integers in Vendure) as a decimal, e.g. `123` => `1.23`
  * * `formatDate`: Formats a Date value with the [dateformat](https://www.npmjs.com/package/dateformat) package.
  *
+ * ## Extending the default email handlers
+ *
+ * The `defaultEmailHandlers` array defines the default handlers such as for handling new account registration, order confirmation, password reset
+ * etc. These defaults can be extended by adding custom templates for languages other than the default, or even completely new types of emails
+ * which respond to any of the available [VendureEvents](/docs/typescript-api/events/). See the {@link EmailEventHandler} documentation for details on how to do so.
+ *
  * ## Dev mode
  *
  * For development, the `transport` option can be replaced by `devMode: true`. Doing so configures Vendure to use the
@@ -159,8 +166,7 @@ export class EmailPlugin implements VendurePlugin {
         }
         const bodySource = await this.templateLoader.loadTemplate(
             type,
-            event.ctx.channel.code,
-            event.ctx.languageCode,
+            result.templateFile,
         );
         const generated = await this.generator.generate(
             result.subject,

+ 3 - 4
packages/email-plugin/src/template-loader.ts

@@ -10,12 +10,11 @@ export class TemplateLoader {
 
     async loadTemplate(
         type: string,
-        channelCode: string,
-        languageCode: LanguageCode,
+        templateFileName: string,
     ): Promise<string> {
         // TODO: logic to select other files based on channel / language
-        const templatePath = path.join(this.templatePath, type, 'body.hbs');
-        const body = await fs.readFile(path.join(this.templatePath, type, 'body.hbs'), 'utf-8');
+        const templatePath = path.join(this.templatePath, type, templateFileName);
+        const body = await fs.readFile(templatePath, 'utf-8');
         return body;
     }
 }

+ 10 - 3
packages/email-plugin/src/types.ts

@@ -5,6 +5,14 @@ import { RequestContext, VendureEvent } from '@vendure/core';
 
 import { EmailEventHandler } from './event-listener';
 
+/**
+ * @description
+ * A VendureEvent which also includes a `ctx` property containing the current
+ * {@link RequestContext}, which is used to determine the channel and language
+ * to use when generating the email.
+ *
+ * @docsCategory EmailPlugin
+ */
 export type EventWithContext = VendureEvent & { ctx: RequestContext; };
 
 /**
@@ -12,6 +20,7 @@ export type EventWithContext = VendureEvent & { ctx: RequestContext; };
  * Configuration for the EmailPlugin.
  *
  * @docsCategory EmailPlugin
+ * @docsWeight 0
  */
 export interface EmailPluginOptions {
     /**
@@ -183,9 +192,7 @@ export type EmailTransportOptions =
 
 /**
  * @description
- * The EmailGenerator uses the {@link EmailContext} and template to generate the email body
- *
- * @docsCategory EmailPlugin
+ * An EmailGenerator generates the subject and body details of an email.
  */
 export interface EmailGenerator<T extends string = any, E extends VendureEvent = any> {
     /**

+ 2 - 0
packages/email-plugin/test-templates/partials/footer.hbs

@@ -0,0 +1,2 @@
+</mj-body>
+</mjml>

+ 6 - 0
packages/email-plugin/test-templates/partials/header.hbs

@@ -0,0 +1,6 @@
+<mjml>
+    <mj-head>
+        <mj-title>Test email</mj-title>
+    </mj-head>
+
+    <mj-body>

+ 9 - 0
packages/email-plugin/test-templates/test/body.de.hbs

@@ -0,0 +1,9 @@
+{{> header }}
+
+<mj-section>
+    <mj-column>
+        <mj-text>German body. {{ testVar }}</mj-text>
+    </mj-column>
+</mj-section>
+
+{{> footer }}

+ 9 - 0
packages/email-plugin/test-templates/test/body.hbs

@@ -0,0 +1,9 @@
+{{> header }}
+
+<mj-section>
+    <mj-column>
+        <mj-text>Default body. {{ testVar }}</mj-text>
+    </mj-column>
+</mj-section>
+
+{{> footer }}