diff --git a/src/app/scaffold.ng.html b/src/app/scaffold.ng.html
index 62264be..e9e6a7c 100644
--- a/src/app/scaffold.ng.html
+++ b/src/app/scaffold.ng.html
@@ -1,4 +1,5 @@
+
diff --git a/src/app/scaffold.spec.ts b/src/app/scaffold.spec.ts
new file mode 100644
index 0000000..a43b59e
--- /dev/null
+++ b/src/app/scaffold.spec.ts
@@ -0,0 +1,129 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {NgIf} from '@angular/common';
+import {Component} from '@angular/core';
+import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing';
+
+import {ScreenshotTest} from '../screenshot_test';
+
+import {DirectedAcyclicGraphModule} from './directed_acyclic_graph';
+import {DagEdge, DagNode} from './node_spec';
+import {DagScaffoldModule} from './scaffold';
+import {AiDagShelf} from './shelf';
+import {initTestBed} from './test_resources/test_utils';
+import {DagToolbarModule} from './toolbar';
+
+const FAKE_NODES: DagNode[] = [
+ new DagNode('step1', 'execution', 'SUCCEEDED'),
+ new DagNode('artifact1', 'artifact', 'SUCCEEDED'),
+ new DagNode('step2', 'execution', 'RUNNING'),
+ new DagNode('step3', 'execution', 'PENDING'),
+ new DagNode('artifact2', 'artifact', 'NO_STATE_RUNTIME'),
+];
+const FAKE_EDGES: DagEdge[] = [
+ {from: 'step1', to: 'artifact1'},
+ {from: 'artifact1', to: 'step2'},
+ {from: 'artifact1', to: 'step3'},
+ {from: 'step2', to: 'artifact2'},
+];
+
+@Component({
+ standalone: false,
+ template: `
+
+
+
+
+
+
+
+ `,
+ styles: [`
+ ai-dag-scaffold {
+ display: block;
+ height: 1000px;
+ }
+ .shelf-content {
+ padding: 10px;
+ background-color: #eee;
+ }
+ `],
+ jit: true,
+})
+class TestComponent {
+ features = {};
+ includeToolbar = true;
+ includeShelf = false;
+ nodes = FAKE_NODES;
+ edges = FAKE_EDGES;
+}
+
+describe('DagScaffold', () => {
+ beforeEach(waitForAsync(async () => {
+ await initTestBed({
+ declarations: [TestComponent],
+ imports: [
+ DagScaffoldModule,
+ DagToolbarModule,
+ DirectedAcyclicGraphModule,
+ AiDagShelf,
+ NgIf,
+ ],
+ });
+ }));
+
+ describe('Component', () => {
+ let fixture: ComponentFixture;
+ let screenShot: ScreenshotTest;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestComponent);
+ fixture.detectChanges();
+ screenShot = new ScreenshotTest(module.id);
+ });
+
+ afterEach(fakeAsync(() => {
+ fixture.destroy();
+ tick();
+ }));
+
+ it('renders without shelf by default', async () => {
+ expect(fixture.nativeElement.querySelector('ai-dag-shelf')).toBeNull();
+ await screenShot.expectMatch('renders_without_shelf_by_default');
+ });
+
+ describe('when shelf is included', () => {
+ beforeEach(fakeAsync(() => {
+ fixture.componentInstance.includeShelf = true;
+ fixture.detectChanges();
+ tick();
+ }));
+
+ it('renders the shelf and its projected content', async () => {
+ const shelfElement =
+ fixture.nativeElement.querySelector('ai-dag-shelf');
+ const shelfContent = shelfElement.querySelector('.shelf-content');
+
+ expect(shelfElement).toBeDefined();
+ expect(shelfContent).toBeDefined();
+ expect(shelfContent.textContent).toBe('');
+ await screenShot.expectMatch('renders_shelf_correctly');
+ });
+ });
+ });
+});
diff --git a/src/app/scuba_goldens/scaffold/chrome-linux/renders_shelf_correctly.png b/src/app/scuba_goldens/scaffold/chrome-linux/renders_shelf_correctly.png
new file mode 100644
index 0000000..f64ee3b
Binary files /dev/null and b/src/app/scuba_goldens/scaffold/chrome-linux/renders_shelf_correctly.png differ
diff --git a/src/app/scuba_goldens/scaffold/chrome-linux/renders_without_shelf_by_default.png b/src/app/scuba_goldens/scaffold/chrome-linux/renders_without_shelf_by_default.png
new file mode 100644
index 0000000..7073786
Binary files /dev/null and b/src/app/scuba_goldens/scaffold/chrome-linux/renders_without_shelf_by_default.png differ
diff --git a/src/app/shelf.ng.html b/src/app/shelf.ng.html
new file mode 100644
index 0000000..6dbc743
--- /dev/null
+++ b/src/app/shelf.ng.html
@@ -0,0 +1 @@
+
diff --git a/src/app/shelf.ts b/src/app/shelf.ts
new file mode 100644
index 0000000..f280053
--- /dev/null
+++ b/src/app/shelf.ts
@@ -0,0 +1,33 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {ChangeDetectionStrategy, Component} from '@angular/core';
+
+/**
+ * Shelf component for DAG graph.
+ */
+@Component({
+ selector: 'ai-dag-shelf',
+ templateUrl: './shelf.ng.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ standalone: true,
+ host: {
+ 'class': 'ai-dag-shelf',
+ },
+})
+export class AiDagShelf {
+}