0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-24 07:43:01 +00:00

Compare commits

...

53 Commits

Author SHA1 Message Date
Víctor Losada Hernández
22e09b0879 Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-11-19 11:00:02 +01:00
Trevor Buckner
6a02ed410b Merge pull request #4534 from G-Ambatte/fixNewNavItem
Update New Item on NavBar
2025-11-17 23:49:42 -05:00
Trevor Buckner
429ad4d63b Linting and move checkLocalStorage inside confirmLocalStorageChange 2025-11-17 23:48:56 -05:00
Trevor Buckner
033893361d Merge branch 'master' into fixNewNavItem 2025-11-17 23:25:44 -05:00
Trevor Buckner
f1ae7e4d26 Merge pull request #4533 from naturalcrit/fix-editor-issues
Fix small bugs towards 3.20
2025-11-17 23:22:04 -05:00
Trevor Buckner
059d6d7939 Lint 2025-11-17 23:09:47 -05:00
G.Ambatte
73b7d6887b Fix typo in state property name for snippet bar height 2025-11-17 17:09:23 +13:00
Víctor Losada Hernández
f1891d9250 use state instead 2025-11-16 19:02:37 +01:00
G.Ambatte
a17ccdb2a2 Merge branch 'master' into fixNewNavItem 2025-11-16 13:19:08 +13:00
G.Ambatte
c784e2e63b Fix New item on Nav Bar 2025-11-16 13:12:40 +13:00
Víctor Losada Hernández
1d061e6d3f Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-editor-issues 2025-11-15 23:41:10 +01:00
Víctor Losada Hernández
5e7fdb34a9 Revert "remove new brew nav item from new page"
This reverts commit b6478f3964.
2025-11-15 23:40:01 +01:00
Víctor Losada Hernández
af477c56b1 last changes 2025-11-15 20:23:03 +01:00
Víctor Losada Hernández
4aacb36b3f Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-11-15 20:06:32 +01:00
Víctor Losada Hernández
677e8eaf6c Merge pull request #4532 from G-Ambatte/fixUnearthedArcanaTheme
Fix UnearthedArcana theme
2025-11-15 17:52:59 +01:00
Víctor Losada Hernández
b6478f3964 remove new brew nav item from new page 2025-11-15 17:25:25 +01:00
Víctor Losada Hernández
f18a73e1ff fix glitch in newPage 2025-11-15 17:25:05 +01:00
Víctor Losada Hernández
b78f5079df bring back update native func 2025-11-15 16:59:32 +01:00
Víctor Losada Hernández
7bc41f9b0d fix snippetbar hidden 2025-11-15 16:49:48 +01:00
Víctor Losada Hernández
a217779e76 fix snippetbar wrap 2025-11-15 16:05:21 +01:00
G.Ambatte
ad3d63a5b1 Merge branch 'master' into fixUnearthedArcanaTheme 2025-11-15 18:44:33 +13:00
G.Ambatte
93ef9bfd51 Set UnearthedArcana baseTheme to Blank 2025-11-15 18:33:22 +13:00
Trevor Buckner
972c675629 Merge pull request #4527 from MiniX16/master
Add unsaved-change warning to Home page editor
2025-11-14 23:04:19 -05:00
Trevor Buckner
fa9f180759 Modify slightly to follow the existing structure in editPage.jsx for easier merging of these pages later 2025-11-14 22:59:34 -05:00
Trevor Buckner
5625121b82 Merge branch 'master' into master 2025-11-14 22:25:47 -05:00
Trevor Buckner
b6065dbcf5 Fix page break in changelog 2025-11-15 03:12:29 +00:00
G.Ambatte
d73b695127 Merge branch 'master' into master 2025-11-15 14:34:13 +13:00
MiniX16
b66625e59d Handle unsaved warning with onbeforeunload 2025-11-13 12:16:37 +01:00
MiniX16
590688f123 Add unsaved-change warning to Home page editor 2025-11-12 17:59:28 +01:00
Víctor Losada Hernández
dd63370d20 Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-11-12 10:00:56 +01:00
Víctor Losada Hernández
9d72796a67 requested changes 2025-11-12 10:00:09 +01:00
Víctor Losada Hernández
6473ea571c Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-07-23 19:53:37 +02:00
Víctor Losada Hernández
2bcd317a4c reworked to be simpler 2025-07-23 19:52:05 +02:00
Víctor Losada Hernández
0ddca82c86 reorder one test 2025-07-18 16:43:49 +02:00
Víctor Losada Hernández
d5645083f3 remove prop drilling 2025-07-18 16:39:00 +02:00
Víctor Losada Hernández
e0bba53df1 Merge branch 'add-remove-author-if-owner' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-07-18 16:22:13 +02:00
Víctor Losada Hernández
223fc0a514 add prop again 2025-07-18 16:20:17 +02:00
Víctor Losada Hernández
625d30f3a8 Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-07-18 16:19:42 +02:00
Trevor Buckner
6388cc7032 Merge branch 'master' into add-remove-author-if-owner 2025-07-17 14:03:04 -04:00
Víctor Losada Hernández
066de435d3 Merge branch 'master' into add-remove-author-if-owner 2025-06-02 12:42:04 +02:00
Víctor Losada Hernández
4ec6ea0f84 fix them? 2025-05-24 22:43:06 +02:00
Víctor Losada Hernández
a7f8ff5212 fix the damn thing 2025-05-24 22:35:29 +02:00
Víctor Losada Hernández
215abbf2f7 fix test 2025-05-24 22:31:11 +02:00
Víctor Losada Hernández
c005d4d387 change test name 2025-05-24 22:29:10 +02:00
Víctor Losada Hernández
fd38371eeb Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-remove-author-if-owner 2025-05-24 22:14:47 +02:00
Víctor Losada Hernández
3c46312929 end tests? 2025-05-24 22:14:28 +02:00
Víctor Losada Hernández
55850f6d3c star tests 2025-05-20 11:39:22 +02:00
Víctor Losada Hernández
790bb5d1b7 lint 2025-05-20 11:39:16 +02:00
Víctor Losada Hernández
c51e8fd9d1 change api route to avoid collision 2025-05-13 08:23:23 +02:00
Víctor Losada Hernández
60714fbf58 lint 2025-05-12 12:27:17 +02:00
Víctor Losada Hernández
f040805d09 server side 2025-05-11 23:59:30 +02:00
Víctor Losada Hernández
c35138e7e3 client side 2025-05-11 23:59:25 +02:00
Víctor Losada Hernández
91f7d86fd4 prop drilling 2025-05-11 23:59:04 +02:00
12 changed files with 336 additions and 210 deletions

View File

@@ -143,6 +143,7 @@ Fixes issue [#2963](https://github.com/naturalcrit/homebrewery/issues/2963)
* [x] Fixed edge case crash on admin page
}}
\page
### Wednesday 7/09/2025 - v3.19.3

View File

@@ -58,7 +58,7 @@ const Editor = createClass({
return {
editorTheme : this.props.editorTheme,
view : 'text', //'text', 'style', 'meta', 'snippet'
snippetbarHeight : 25
snippetBarHeight : 26,
};
},
@@ -85,7 +85,15 @@ const Editor = createClass({
editorTheme : editorTheme
});
}
this.setState({ snippetbarHeight: document.querySelector('.editor > .snippetBar').offsetHeight });
const snippetBar = document.querySelector('.editor > .snippetBar');
if (!snippetBar) return;
this.resizeObserver = new ResizeObserver(entries => {
const height = document.querySelector('.editor > .snippetBar').offsetHeight;
this.setState({ snippetBarHeight: height });
});
this.resizeObserver.observe(snippetBar);
},
componentDidUpdate : function(prevProps, prevState, snapshot) {
@@ -108,6 +116,10 @@ const Editor = createClass({
}
},
componentWillUnmount() {
if (this.resizeObserver) this.resizeObserver.disconnect();
},
handleControlKeys : function(e){
if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return;
const LEFTARROW_KEY = 37;
@@ -408,11 +420,7 @@ const Editor = createClass({
},
//Called when there are changes to the editor's dimensions
update : function(){
const snipHeight = document.querySelector('.editor > .snippetBar').offsetHeight;
if(snipHeight !== this.state.snippetbarHeight)
this.setState({ snippetbarHeight: snipHeight });
},
update : function(){},
updateEditorTheme : function(newTheme){
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme);
@@ -437,7 +445,7 @@ const Editor = createClass({
onChange={this.props.onBrewChange('text')}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
</>;
}
if(this.isStyle()){
@@ -451,7 +459,7 @@ const Editor = createClass({
enableFolding={true}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
style={{ height: `calc(100% - ${this.state.snippetBarHeight}px)` }} />
</>;
}
if(this.isMeta()){
@@ -468,7 +476,6 @@ const Editor = createClass({
userThemes={this.props.userThemes}/>
</>;
}
if(this.isSnip()){
if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; }
return <>
@@ -481,7 +488,7 @@ const Editor = createClass({
enableFolding={true}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
style={{ height: `calc(100% -${this.state.snippetBarHeight}px)` }} />
</>;
}
},

View File

@@ -47,6 +47,7 @@ const MetadataEditor = createClass({
getInitialState : function(){
return {
isOwner : global.account?.username && global.account?.username === this.props.metadata?.authors[0],
showThumbnail : true
};
},
@@ -156,6 +157,15 @@ const MetadataEditor = createClass({
});
},
handleDeleteAuthor : function(author){
if(!confirm('Are you sure you want to remove this author? They will lose all edit access to this brew, and it will dissapear from their userpage.')) return;
if(!this.props.metadata.authors.includes(author)) return;
this.props.onChange({
...this.props.metadata,
authors : this.props.metadata.authors.filter((a)=>a !== author)
});
},
renderSystems : function(){
return _.map(SYSTEMS, (val)=>{
return <label key={val}>
@@ -194,16 +204,54 @@ const MetadataEditor = createClass({
},
renderAuthors : function(){
let text = 'None.';
if(this.props.metadata.authors && this.props.metadata.authors.length){
text = this.props.metadata.authors.join(', ');
}
return <div className='field authors'>
<label>authors</label>
<div className='value'>
{text}
const authors = this.props.metadata.authors;
if(!this.state.isOwner || authors.length < 2) return (
<div className='field authors'>
<label>authors</label>
<div className='value'>
{authors.length > 0 && (
<a href={`/user/${authors[0]}`} className='author-link' title={`Owner - Click to open ${authors[0]}'s profile in a new tab`}>
{authors[0]}{authors.length > 1 && ', '}
</a>
)}
{authors.length > 1 && authors.slice(1).map((author, i)=>(
<a href={`/user/${author}`} className='author-link' title={`Author - Click to open ${author}'s profile in a new tab`}>
{author}{i+2 < authors.length && ', '}
</a>
))}
</div>
</div>
</div>;
);
return (
<div className='field authors'>
<label>Authors</label>
<ul className='list'>
{authors.length > 0 && (
<li className='tag owner' title='Owner'>
<a href={`/user/${authors[0]}`} className='author-link' title={`Owner - Click to open ${authors[0]}'s profile in a new tab`}>
{authors[0]}
</a>
</li>
)}
{authors.length > 1 && authors.slice(1).map((author, i)=>(
<li className='tag author' key={i + 1} title='Author'>
<a href={`/user/${author}`} className='author-link' title={`Author - Click to open ${authors[0]}'s profile in a new tab`}>
{author}
</a>
<button
onClick={()=>this.handleDeleteAuthor(author)}
className='delete'
title={`Remove ${author} as an author`}
>
<i className='fa fa-times fa-fw' />
</button>
</li>
))}
</ul>
</div>
);
},
renderThemeDropdown : function(){

View File

@@ -44,8 +44,6 @@
gap : 10px;
}
.field {
position : relative;
display : flex;
@@ -116,7 +114,6 @@
}
}
.thumbnail-preview {
position : relative;
flex : 1 1;
@@ -164,7 +161,47 @@
.colorButton(@red);
}
}
.authors.field .value { line-height : 1.5em; }
.authors.field {
.tag {
font-weight:300;
transition:background-color 0.2s;
&.owner {
position: relative;
background-color:@silverLight;
min-width:25px;
display:grid;
place-items:center;
font-weight: 900;
&::after {
content: "\f521";
font-family: "Font Awesome 6 Free";
color:gold;
position: absolute;
top: 0;
left: 0;
width:15px;
height:15px;
rotate:-45deg;
translate:-50% -50%;
}
}
&:has(button:hover) {
background:#d97d7d;
}
button {
color:@red;
}
}
a {
color:black;
text-decoration:unset;
}
}
.themes.field {
& .dropdown-container {
@@ -266,13 +303,17 @@
}
.tag {
padding : 0.3em;
padding : 0.35em;
margin : 2px;
font-size : 0.9em;
font-size : 0.95em;
background-color : #DDDDDD;
border-radius : 0.5em;
.icon { #groupedIcon; }
button {
cursor : pointer;
}
}
.input-group {

View File

@@ -14,7 +14,7 @@
.snippets {
display : flex;
justify-content : flex-start;
min-width : 432.18px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
min-width : 499.35px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
}
.editors {
@@ -237,7 +237,7 @@
}
}
@container editor (width < 683px) {
@container editor (width < 750px) {
.snippetBar {
.editors {
flex : 1;

View File

@@ -3,19 +3,16 @@ const _ = require('lodash');
const Nav = require('client/homebrew/navbar/nav.jsx');
const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); // Importing the function from helpers.js
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
const METAKEY = 'HB_newPage_meta';
const NewBrew = ()=>{
const handleFileChange = (e)=>{
const file = e.target.files[0];
if(!file) return;
const currentNew = localStorage.getItem(BREWKEY);
if(currentNew && !confirm(
`You have some text in the new brew space, if you load a file that text will be lost, are you sure you want to load the file?`
)) return;
if(!confirmLocalStorageChange()) return;
const reader = new FileReader();
reader.onload = (e)=>{
@@ -37,12 +34,35 @@ const NewBrew = ()=>{
alert(`This file is invalid: ${!type ? 'Missing file extension' :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`);
console.log(file);
};
reader.readAsText(file);
};
const confirmLocalStorageChange = ()=>{
const currentText = localStorage.getItem(BREWKEY);
const currentStyle = localStorage.getItem(STYLEKEY);
const currentMeta = localStorage.getItem(METAKEY);
// TRUE if no data in any local storage key
// TRUE if data in any local storage key AND approval given
// FALSE if data in any local storage key AND approval declined
return (!(currentText || currentStyle || currentMeta) || confirm(
`You have made changes in the new brew space. If you continue, that information will be PERMANENTLY LOST.\nAre you sure you wish to continue?`
));
};
const clearLocalStorage = ()=>{
if(!confirmLocalStorageChange()) return;
localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY);
window.location.href = '/new';
return;
};
return (
<Nav.dropdown>
@@ -50,24 +70,31 @@ const NewBrew = ()=>{
className='new'
color='purple'
icon='fa-solid fa-plus-square'>
new
new
</Nav.item>
<Nav.item
className='fromBlank'
className='new'
href='/new'
newTab={true}
color='purple'
icon='fa-solid fa-file'>
from blank
resume draft
</Nav.item>
<Nav.item
className='fromBlank'
newTab={true}
color='yellow'
icon='fa-solid fa-file-circle-plus'
onClick={()=>{ clearLocalStorage(); }}>
from blank
</Nav.item>
<Nav.item
className='fromFile'
color='purple'
color='green'
icon='fa-solid fa-upload'
onClick={()=>{ document.getElementById('uploadTxt').click(); }}>
<input id='uploadTxt' className='newFromLocal' type='file' onChange={handleFileChange} style={{ display: 'none' }} />
from file
from file
</Nav.item>
</Nav.dropdown>
);

View File

@@ -53,8 +53,9 @@ const HomePage =(props)=>{
const [isSaving , setIsSaving] = useState(false);
const [autoSaveEnabled , setAutoSaveEnable] = useState(false);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const unsavedChangesRef = useRef(unsavedChanges);
useEffect(()=>{
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
@@ -70,12 +71,20 @@ const HomePage =(props)=>{
};
document.addEventListener('keydown', handleControlKeys);
window.onbeforeunload = ()=>{
if(unsavedChangesRef.current)
return 'You have unsaved changes!';
};
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
window.onbeforeunload = null;
};
}, []);
useEffect(()=>{
unsavedChangesRef.current = unsavedChanges;
}, [unsavedChanges]);
const save = ()=>{
request.post('/api')
.send(currentBrew)

View File

@@ -1,12 +1,15 @@
.newPage {
.navItem.save {
.fadeInRight();
.transition(opacity);
background-color : @orange;
transition:all 0.2s;
&:hover { background-color : @green; }
&.neverSaved {
.fadeOutRight();
translate:-100%;
opacity: 0;
background-color :#333;
cursor:auto;
}
}
}

View File

@@ -171,7 +171,6 @@ const api = {
next();
};
},
getCSS : async (req, res)=>{
const { brew } = req;
if(!brew) return res.status(404).send('');
@@ -184,7 +183,6 @@ const api = {
});
return res.status(200).send(brew.style);
},
mergeBrewText : (brew)=>{
let text = brew.text;
if(brew.style !== undefined) {
@@ -202,7 +200,6 @@ const api = {
`${text}`;
return text;
},
getGoodBrewTitle : (text)=>{
const tokens = Markdown.marked.lexer(text);
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')

View File

@@ -204,7 +204,6 @@ describe('Tests for api', ()=>{
expect(id).toEqual('abcdefghij');
});
});
describe('getBrew', ()=>{
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
const notFoundError = { HBErrorCode: '05', message: 'Brew not found', name: 'BrewLoad Error', status: 404, accessType: 'share', brewId: '1' };
@@ -382,7 +381,68 @@ describe('Tests for api', ()=>{
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '51', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
});
});
describe('Get CSS', ()=>{
it('should return brew style content as CSS text', async ()=>{
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n```\n\n' };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
expect(res.set).toHaveBeenCalledWith({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/css'
});
});
it('should return 404 when brew has no style content', async ()=>{
const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith('');
});
it('should return 404 when brew does not exist', async ()=>{
const testBrew = { };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith('');
});
});
describe('mergeBrewText', ()=>{
it('should set metadata and no style if it is not present', ()=>{
const result = api.mergeBrewText({
@@ -445,7 +505,6 @@ hello yes i am css
brew`);
});
});
describe('exclusion methods', ()=>{
it('excludePropsFromUpdate removes the correct keys', ()=>{
const sent = Object.assign({}, googleBrew);
@@ -483,7 +542,6 @@ brew`);
expect(result.pageCount).toBe(1);
});
});
describe('beforeNewSave', ()=>{
it('sets the title if none', ()=>{
const brew = {
@@ -525,7 +583,6 @@ brew`);
expect(hbBrew.text).toEqual('merged');
});
});
describe('newGoogleBrew', ()=>{
it('should call the correct methods', ()=>{
api.excludeGoogleProps = jest.fn(()=>'newBrew');
@@ -539,7 +596,6 @@ brew`);
expect(google.newGoogleBrew).toHaveBeenCalledWith('client', 'newBrew');
});
});
describe('newBrew', ()=>{
it('should set up a default brew via Homebrew model', async ()=>{
await api.newBrew({ body: { text: 'asdf' }, query: {}, account: { username: 'test user' } }, res);
@@ -631,17 +687,6 @@ brew`);
});
});
});
describe('deleteGoogleBrew', ()=>{
it('should check auth and delete brew', async ()=>{
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
expect(result).toBe(true);
expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
});
});
describe('Theme bundle', ()=>{
it('should return Theme Bundle for a User Theme', async ()=>{
const brews = {
@@ -785,7 +830,94 @@ brew`);
status : 422 });
});
});
describe('updateBrew', ()=>{
it('should return error on version mismatch', async ()=>{
const brewFromClient = { version: 1 };
const brewFromServer = { version: 1000, text: '' };
const req = {
brew : brewFromServer,
body : brewFromClient
};
await api.updateBrew(req, res);
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
});
it('should return error on hash mismatch', async ()=>{
const brewFromClient = { version: 1, hash: '1234' };
const brewFromServer = { version: 1, text: 'test' };
const req = {
brew : brewFromServer,
body : brewFromClient
};
await api.updateBrew(req, res);
expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
});
// Commenting this one out for now, since we are no longer throwing this error while we monitor
// it('should return error on applying patches', async ()=>{
// const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
// const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
// const req = {
// brew : brewFromServer,
// body : brewFromClient,
// };
// let err;
// try {
// await api.updateBrew(req, res);
// } catch (e) {
// err = e;
// }
// expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
// });
it('should save brew, no ID', async ()=>{
const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
model.save = jest.fn((brew)=>{return brew;});
const req = {
brew : brewFromServer,
body : brewFromClient,
query : { saveToGoogle: false, removeFromGoogle: false }
};
await api.updateBrew(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith(
expect.objectContaining({
_id : '1',
description : 'Test Description',
hash : '098f6bcd4621d373cade4e832627b4f6',
title : 'Test Title',
version : 2
})
);
});
});
describe('deleteGoogleBrew', ()=>{
it('should check auth and delete brew', async ()=>{
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
expect(result).toBe(true);
expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
});
});
describe('deleteBrew', ()=>{
it('should handle case where fetching the brew returns an error', async ()=>{
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
@@ -1006,68 +1138,7 @@ brew`);
expect(saved.googleId).toEqual(brew.googleId);
});
});
describe('Get CSS', ()=>{
it('should return brew style content as CSS text', async ()=>{
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n```\n\n' };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
expect(res.set).toHaveBeenCalledWith({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/css'
});
});
it('should return 404 when brew has no style content', async ()=>{
const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith('');
});
it('should return 404 when brew does not exist', async ()=>{
const testBrew = { };
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise(testBrew));
const fn = api.getBrew('share', true);
const req = { brew: {} };
const next = jest.fn();
await fn(req, null, next);
await api.getCSS(req, res);
expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style');
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith('');
});
});
describe('Split Text, Style, and Metadata', ()=>{
it('basic splitting', async ()=>{
@@ -1121,83 +1192,5 @@ brew`);
expect(testBrew.tags).toEqual(['tag a']);
});
});
describe('updateBrew', ()=>{
it('should return error on version mismatch', async ()=>{
const brewFromClient = { version: 1 };
const brewFromServer = { version: 1000, text: '' };
const req = {
brew : brewFromServer,
body : brewFromClient
};
await api.updateBrew(req, res);
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
});
it('should return error on hash mismatch', async ()=>{
const brewFromClient = { version: 1, hash: '1234' };
const brewFromServer = { version: 1, text: 'test' };
const req = {
brew : brewFromServer,
body : brewFromClient
};
await api.updateBrew(req, res);
expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
});
// Commenting this one out for now, since we are no longer throwing this error while we monitor
// it('should return error on applying patches', async ()=>{
// const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
// const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
// const req = {
// brew : brewFromServer,
// body : brewFromClient,
// };
// let err;
// try {
// await api.updateBrew(req, res);
// } catch (e) {
// err = e;
// }
// expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
// });
it('should save brew, no ID', async ()=>{
const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
model.save = jest.fn((brew)=>{return brew;});
const req = {
brew : brewFromServer,
body : brewFromClient,
query : { saveToGoogle: false, removeFromGoogle: false }
};
await api.updateBrew(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith(
expect.objectContaining({
_id : '1',
description : 'Test Description',
hash : '098f6bcd4621d373cade4e832627b4f6',
title : 'Test Title',
version : 2
})
);
});
});
});

View File

@@ -1,6 +1,6 @@
{
"name" : "UnearthedArcana",
"renderer" : "V3",
"baseTheme" : false,
"baseTheme" : "Blank",
"baseSnippets" : false
}

View File

@@ -39,7 +39,7 @@
"UnearthedArcana": {
"name": "UnearthedArcana",
"renderer": "V3",
"baseTheme": false,
"baseTheme": "Blank",
"baseSnippets": false,
"path": "UnearthedArcana"
}